1use std::env;
2use std::ffi::OsStr;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use dirs::{config_dir, home_dir};
7
8use crate::error::ShellError;
9
10#[derive(Debug)]
11pub enum Shell {
40 POSIX(POSIX),
41 Zsh(Zsh),
42 Bash(Bash),
43 Fish(Fish),
44}
45
46impl Shell {
47 pub fn detect_by_shell_var() -> Result<Shell, ShellError> {
67 if cfg!(windows) {
68 return Err(ShellError::UnsupportedPlatform);
69 }
70
71 let shell = env::var("SHELL").map_err(|_| ShellError::NoShellVar)?;
72
73 match shell.as_str() {
74 path if path.contains("zsh") => Ok(Shell::Zsh(Zsh)),
75 path if path.contains("bash") => Ok(Shell::Bash(Bash)),
76 path if path.contains("fish") => Ok(Shell::Fish(Fish)),
77 _ => Ok(Shell::POSIX(POSIX)),
78 }
79 }
80
81 pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
82 match self {
83 Shell::Fish(fish) => fish.get_rcfiles(),
84 Shell::Zsh(zsh) => zsh.get_rcfiles(),
85 Shell::Bash(bash) => bash.get_rcfiles(),
86 Shell::POSIX(posix) => posix.get_rcfiles(),
87 }
88 }
89}
90
91#[derive(Debug)]
92pub struct POSIX;
93
94impl POSIX {
95 pub fn does_exist(&self) -> bool {
96 true
97 }
98 pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
99 let dir = home_dir().ok_or(ShellError::NoHomeDir)?;
100 Ok(vec![dir.join(".profile")])
101 }
102 pub fn get_rcfiles_from_base(base_dir: impl AsRef<Path>) -> Vec<PathBuf> {
103 vec![base_dir.as_ref().join(".profile")]
104 }
105}
106
107#[derive(Debug)]
108pub struct Zsh;
109
110impl Zsh {
111 pub fn does_exist(&self) -> bool {
112 matches!(env::var("SHELL"), Ok(v) if v.contains("zsh"))
113 || Command::new("zsh").output().is_ok()
114 }
115
116 pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
117 let mut rc_files = Vec::new();
118
119 if let Ok(output) = std::process::Command::new("zsh")
121 .args(["-c", "echo -n $ZDOTDIR"])
122 .output()
123 {
124 if !output.stdout.is_empty() {
125 if let Ok(zdotdir) = String::from_utf8(output.stdout) {
126 let path = PathBuf::from(zdotdir.trim()).join(".zshenv");
127 if path.exists() {
128 rc_files.push(path);
129 }
130 }
131 }
132 }
133
134 if let Ok(home) = std::env::var("HOME") {
136 let path = PathBuf::from(home).join(".zshenv");
137 if path.exists() {
138 rc_files.push(path);
139 }
140 }
141
142 if rc_files.is_empty() {
143 Err(ShellError::EmptyHomeAndZdotdir)
144 } else {
145 Ok(rc_files)
146 }
147 }
148 pub fn get_rcfiles_from_base(base_dir: impl AsRef<Path>) -> Vec<PathBuf> {
149 vec![base_dir.as_ref().join(".zshenv")]
150 }
151}
152
153#[derive(Debug)]
154pub struct Bash;
155
156impl Bash {
157 pub fn does_exist(&self) -> bool {
158 matches!(env::var("SHELL"), Ok(v) if v.contains("bash"))
159 || Command::new("bash").output().is_ok()
160 }
161
162 pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
163 let dir = home_dir().ok_or(ShellError::NoHomeDir)?;
164 let rcfiles = [".bash_profile", ".bash_login", ".bashrc"]
165 .iter()
166 .map(|rc| dir.join(rc))
167 .collect();
168 Ok(rcfiles)
169 }
170
171 pub fn get_rcfiles_from_base(base_dir: impl AsRef<Path>) -> Vec<PathBuf> {
172 [".bash_profile", ".bash_login", ".bashrc"]
173 .iter()
174 .map(|rc| base_dir.as_ref().join(rc))
175 .collect()
176 }
177}
178
179#[derive(Debug)]
180pub struct Fish;
181
182impl Fish {
183 pub fn does_exist(&self) -> bool {
184 matches!(env::var("SHELL"), Ok(v) if v.contains("fish"))
185 || Command::new("fish").output().is_ok()
186 }
187
188 pub fn get_rcfiles(&self) -> Result<Vec<PathBuf>, ShellError> {
210 let mut paths = vec![];
211
212 if let Some(path) = config_dir() {
213 paths.push(path.join("fish/conf.d"));
214 }
215
216 Ok(paths)
217 }
218
219 pub fn get_rcfiles_from_base(base_dir: impl AsRef<Path>) -> Vec<PathBuf> {
220 vec![base_dir.as_ref().join(".config/fish/conf.d")]
221 }
222}
223
224pub fn exists_in_path(path: impl AsRef<Path>) -> bool {
225 matches!(env::var("PATH"), Ok(paths) if paths.contains(path.as_ref().to_str().unwrap()))
226}
227
228pub fn append_to_rcfile(rcfile: PathBuf, line: &str) -> Result<(), ShellError> {
229 use std::fs::OpenOptions;
230 use std::io::Write;
231
232 if !rcfile.exists() {
233 return Err(ShellError::RCFileNotFound(
234 rcfile
235 .file_name()
236 .unwrap_or_else(|| OsStr::new("unknown"))
237 .to_string_lossy()
238 .into_owned(),
239 ));
240 }
241
242 let mut file = OpenOptions::new().append(true).open(rcfile).unwrap();
243 writeln!(file, "{}", line)?;
244 Ok(())
245}
246
247pub fn remove_from_rcfile(rcfile: PathBuf, line: &str) -> Result<(), ShellError> {
248 if !rcfile.exists() {
249 return Err(ShellError::RCFileNotFound(
250 rcfile
251 .file_name()
252 .unwrap_or_else(|| OsStr::new("unknown"))
253 .to_string_lossy()
254 .into_owned(),
255 ));
256 }
257
258 let line_bytes = line.as_bytes();
259
260 let file = std::fs::read_to_string(&rcfile)?;
261 let file_bytes = file.as_bytes();
262
263 if let Some(idx) = file_bytes
264 .windows(line_bytes.len())
265 .position(|w| w == line_bytes)
266 {
267 let mut new_bytes = file_bytes[..idx].to_vec();
268 new_bytes.extend(&file_bytes[idx + line_bytes.len()..]);
269 let content = String::from_utf8(new_bytes).unwrap();
270 std::fs::write(&rcfile, content)?;
271 }
272
273 Ok(())
274}