1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
11pub enum Shell {
12 #[default]
14 Bash,
15 Zsh,
17 Fish,
19 #[serde(rename = "powershell")]
21 PowerShell,
22}
23
24impl Shell {
25 pub fn detect(target: Option<&str>) -> Self {
27 if let Some(t) = target {
28 return Self::parse(t);
29 }
30
31 if let Ok(shell) = std::env::var("SHELL") {
33 if shell.contains("fish") {
34 return Shell::Fish;
35 } else if shell.contains("zsh") {
36 return Shell::Zsh;
37 } else if shell.contains("bash") {
38 return Shell::Bash;
39 }
40 }
41
42 Shell::Bash
44 }
45
46 pub fn parse(s: &str) -> Self {
48 match s.to_lowercase().as_str() {
49 "zsh" => Shell::Zsh,
50 "fish" => Shell::Fish,
51 "powershell" | "pwsh" => Shell::PowerShell,
52 _ => Shell::Bash,
53 }
54 }
55
56 pub fn name(&self) -> &'static str {
58 match self {
59 Shell::Bash => "bash",
60 Shell::Zsh => "zsh",
61 Shell::Fish => "fish",
62 Shell::PowerShell => "powershell",
63 }
64 }
65
66 pub fn is_supported(&self) -> bool {
68 matches!(self, Shell::Bash | Shell::Zsh | Shell::Fish)
69 }
70}
71
72impl fmt::Display for Shell {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "{}", self.name())
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn test_shell_default() {
84 let shell = Shell::default();
85 assert_eq!(shell, Shell::Bash);
86 }
87
88 #[test]
89 fn test_shell_parse() {
90 assert_eq!(Shell::parse("bash"), Shell::Bash);
91 assert_eq!(Shell::parse("zsh"), Shell::Zsh);
92 assert_eq!(Shell::parse("fish"), Shell::Fish);
93 assert_eq!(Shell::parse("powershell"), Shell::PowerShell);
94 assert_eq!(Shell::parse("pwsh"), Shell::PowerShell);
95 assert_eq!(Shell::parse("unknown"), Shell::Bash);
96 }
97
98 #[test]
99 fn test_shell_parse_case_insensitive() {
100 assert_eq!(Shell::parse("BASH"), Shell::Bash);
101 assert_eq!(Shell::parse("ZSH"), Shell::Zsh);
102 assert_eq!(Shell::parse("Fish"), Shell::Fish);
103 assert_eq!(Shell::parse("PowerShell"), Shell::PowerShell);
104 assert_eq!(Shell::parse("PWSH"), Shell::PowerShell);
105 }
106
107 #[test]
108 fn test_shell_detect_with_target() {
109 assert_eq!(Shell::detect(Some("bash")), Shell::Bash);
110 assert_eq!(Shell::detect(Some("zsh")), Shell::Zsh);
111 assert_eq!(Shell::detect(Some("fish")), Shell::Fish);
112 assert_eq!(Shell::detect(Some("powershell")), Shell::PowerShell);
113 }
114
115 #[test]
116 fn test_shell_detect_from_env_fish() {
117 temp_env::with_var("SHELL", Some("/usr/bin/fish"), || {
118 let shell = Shell::detect(None);
119 assert_eq!(shell, Shell::Fish);
120 });
121 }
122
123 #[test]
124 fn test_shell_detect_from_env_zsh() {
125 temp_env::with_var("SHELL", Some("/bin/zsh"), || {
126 let shell = Shell::detect(None);
127 assert_eq!(shell, Shell::Zsh);
128 });
129 }
130
131 #[test]
132 fn test_shell_detect_from_env_bash() {
133 temp_env::with_var("SHELL", Some("/bin/bash"), || {
134 let shell = Shell::detect(None);
135 assert_eq!(shell, Shell::Bash);
136 });
137 }
138
139 #[test]
140 fn test_shell_detect_default_fallback() {
141 temp_env::with_var_unset("SHELL", || {
142 let shell = Shell::detect(None);
143 assert_eq!(shell, Shell::Bash);
144 });
145 }
146
147 #[test]
148 fn test_shell_name() {
149 assert_eq!(Shell::Bash.name(), "bash");
150 assert_eq!(Shell::Zsh.name(), "zsh");
151 assert_eq!(Shell::Fish.name(), "fish");
152 assert_eq!(Shell::PowerShell.name(), "powershell");
153 }
154
155 #[test]
156 fn test_shell_support() {
157 assert!(Shell::Bash.is_supported());
158 assert!(Shell::Zsh.is_supported());
159 assert!(Shell::Fish.is_supported());
160 assert!(!Shell::PowerShell.is_supported());
161 }
162
163 #[test]
164 fn test_shell_display() {
165 assert_eq!(format!("{}", Shell::Bash), "bash");
166 assert_eq!(format!("{}", Shell::Zsh), "zsh");
167 assert_eq!(format!("{}", Shell::Fish), "fish");
168 assert_eq!(format!("{}", Shell::PowerShell), "powershell");
169 }
170
171 #[test]
172 fn test_shell_serde_roundtrip() {
173 let shells = vec![Shell::Bash, Shell::Zsh, Shell::Fish, Shell::PowerShell];
174 for shell in shells {
175 let json = serde_json::to_string(&shell).unwrap();
176 let parsed: Shell = serde_json::from_str(&json).unwrap();
177 assert_eq!(shell, parsed);
178 }
179 }
180
181 #[test]
182 fn test_shell_serde_powershell_rename() {
183 let shell = Shell::PowerShell;
184 let json = serde_json::to_string(&shell).unwrap();
185 assert_eq!(json, "\"powershell\"");
186 }
187
188 #[test]
189 fn test_shell_clone() {
190 let shell = Shell::Fish;
191 let cloned = shell;
192 assert_eq!(shell, cloned);
193 }
194
195 #[test]
196 fn test_shell_copy() {
197 let shell = Shell::Zsh;
198 let copied = shell;
199 assert_eq!(shell, copied);
200 }
201
202 #[test]
203 fn test_shell_debug() {
204 let debug = format!("{:?}", Shell::Bash);
205 assert!(debug.contains("Bash"));
206 }
207}