1use crate::pkg::resolve::SourceType;
2use anyhow::anyhow;
3use colored::*;
4use std::fmt::Display;
5use std::fs;
6use std::io::{Write, stdin, stdout};
7use std::process::Command;
8use std::time::Duration;
9use walkdir::WalkDir;
10
11pub fn format_bytes(bytes: u64) -> String {
12 const KIB: u64 = 1024;
13 const MIB: u64 = 1024 * KIB;
14 const GIB: u64 = 1024 * MIB;
15
16 if bytes >= GIB {
17 format!("{:.2} GiB", bytes as f64 / GIB as f64)
18 } else if bytes >= MIB {
19 format!("{:.2} MiB", bytes as f64 / MIB as f64)
20 } else if bytes >= KIB {
21 format!("{:.2} KiB", bytes as f64 / KIB as f64)
22 } else {
23 format!("{} B", bytes)
24 }
25}
26
27pub fn format_size_diff(diff: i64) -> String {
28 if diff == 0 {
29 return "0 B".to_string();
30 }
31
32 let sign = if diff > 0 { "+" } else { "-" };
33 let bytes = diff.unsigned_abs();
34
35 format!("{} {}", sign, format_bytes(bytes))
36}
37
38use crate::pkg::types::Scope;
39use clap_complete::Shell;
40use std::path::{Path, PathBuf};
41
42pub fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
43 fs::create_dir_all(dst)?;
44 for entry in fs::read_dir(src)? {
45 let entry = entry?;
46 let ty = entry.file_type()?;
47 if ty.is_dir() {
48 copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?;
49 } else {
50 fs::copy(entry.path(), dst.join(entry.file_name()))?;
51 }
52 }
53 Ok(())
54}
55
56pub fn is_admin() -> bool {
57 #[cfg(windows)]
58 {
59 use std::mem;
60 use std::ptr;
61 use winapi::um::handleapi::CloseHandle;
62 use winapi::um::processthreadsapi::GetCurrentProcess;
63 use winapi::um::processthreadsapi::OpenProcessToken;
64 use winapi::um::securitybaseapi::CheckTokenMembership;
65 use winapi::um::winnt::{PSID, TOKEN_QUERY};
66
67 let mut token = ptr::null_mut();
68 let process = unsafe { GetCurrentProcess() };
69 if unsafe { OpenProcessToken(process, TOKEN_QUERY, &mut token) } == 0 {
70 return false;
71 }
72
73 let mut sid: [u8; 8] = [0; 8];
74 let mut sid_size = mem::size_of_val(&sid) as u32;
75 if unsafe {
76 winapi::um::securitybaseapi::CreateWellKnownSid(
77 winapi::um::winnt::WinBuiltinAdministratorsSid,
78 ptr::null_mut(),
79 sid.as_mut_ptr() as PSID,
80 &mut sid_size,
81 )
82 } == 0
83 {
84 unsafe { CloseHandle(token) };
85 return false;
86 }
87
88 let mut is_member = 0;
89 let result =
90 unsafe { CheckTokenMembership(token, sid.as_mut_ptr() as PSID, &mut is_member) };
91 unsafe { CloseHandle(token) };
92
93 result != 0 && is_member != 0
94 }
95 #[cfg(unix)]
96 {
97 nix::unistd::getuid().is_root()
98 }
99}
100
101pub fn print_info<T: Display>(key: &str, value: T) {
102 println!("{}: {}", key, value);
103}
104
105pub fn format_version_summary(branch: &str, status: &str, number: &str) -> String {
106 let branch_short = if branch == "Production" {
107 "Prod."
108 } else if branch == "Development" {
109 "Dev."
110 } else if branch == "Public" {
111 "Pub."
112 } else if branch == "Special" {
113 "Spec."
114 } else {
115 branch
116 };
117 format!(
118 "{} {} {}",
119 branch_short.blue().bold().italic(),
120 status,
121 number,
122 )
123}
124
125pub fn format_version_full(branch: &str, status: &str, number: &str, commit: &str) -> String {
126 format!(
127 "{} {}",
128 format_version_summary(branch, status, number),
129 commit.green()
130 )
131}
132
133pub fn print_aligned_info(key: &str, value: &str) {
134 let key_with_colon = format!("{}:", key);
135 println!("{:<18}{}", key_with_colon.cyan(), value);
136}
137
138pub fn run_shell_command(command_str: &str) -> anyhow::Result<()> {
139 let status = if cfg!(target_os = "windows") {
140 Command::new("pwsh")
141 .arg("-Command")
142 .arg(command_str)
143 .status()?
144 } else {
145 Command::new("bash").arg("-c").arg(command_str).status()?
146 };
147
148 if !status.success() {
149 return Err(anyhow!("Command failed: {}", command_str));
150 }
151 Ok(())
152}
153
154pub fn command_exists(command: &str) -> bool {
155 if cfg!(target_os = "windows") {
156 Command::new("where")
157 .arg(command)
158 .stdout(std::process::Stdio::null())
159 .stderr(std::process::Stdio::null())
160 .status()
161 .is_ok_and(|status| status.success())
162 } else {
163 Command::new("bash")
164 .arg("-c")
165 .arg(format!("command -v {}", command))
166 .stdout(std::process::Stdio::null())
167 .stderr(std::process::Stdio::null())
168 .status()
169 .is_ok_and(|status| status.success())
170 }
171}
172
173pub fn ask_for_confirmation(prompt: &str, yes: bool) -> bool {
174 if yes {
175 return true;
176 }
177 print!("{} [y/N]: ", prompt.yellow());
178 let _ = stdout().flush();
179 let mut input = String::new();
180 if stdin().read_line(&mut input).is_err() {
181 return false;
182 }
183 input.trim().eq_ignore_ascii_case("y")
184}
185
186pub fn set_path_read_only(path: &Path) -> anyhow::Result<()> {
187 if !path.exists() {
188 return Ok(());
189 }
190 for entry in WalkDir::new(path) {
191 let entry = entry?;
192 let mut perms = fs::metadata(entry.path())?.permissions();
193 if !perms.readonly() {
194 perms.set_readonly(true);
195 fs::set_permissions(entry.path(), perms)?;
196 }
197 }
198 Ok(())
199}
200
201pub fn set_path_writable(path: &Path) -> anyhow::Result<()> {
202 if !path.exists() {
203 return Ok(());
204 }
205 for entry in WalkDir::new(path) {
206 let entry = entry?;
207 let mut perms = fs::metadata(entry.path())?.permissions();
208 if perms.readonly() {
209 #[cfg(unix)]
210 {
211 use std::os::unix::fs::PermissionsExt;
212 let mode = perms.mode();
213 perms.set_mode(mode | 0o200);
214 }
215 #[cfg(not(unix))]
216 {
217 perms.set_readonly(false);
218 }
219 fs::set_permissions(entry.path(), perms)?;
220 }
221 }
222 Ok(())
223}
224
225use std::collections::HashMap;
226
227pub fn get_linux_distribution_info() -> Option<HashMap<String, String>> {
228 if let Ok(contents) = fs::read_to_string("/etc/os-release") {
229 let info: HashMap<String, String> = contents
230 .lines()
231 .filter_map(|line| {
232 let mut parts = line.splitn(2, '=');
233 let key = parts.next()?;
234 let value = parts.next()?.trim_matches('"').to_string();
235 if key.is_empty() {
236 None
237 } else {
238 Some((key.to_string(), value))
239 }
240 })
241 .collect();
242 if info.is_empty() { None } else { Some(info) }
243 } else {
244 None
245 }
246}
247
248pub fn get_linux_distro_family() -> Option<String> {
249 if let Some(info) = get_linux_distribution_info() {
250 if let Some(id_like) = info.get("ID_LIKE") {
251 let families: Vec<&str> = id_like.split_whitespace().collect();
252 if families.contains(&"debian") {
253 return Some("debian".to_string());
254 }
255 if families.contains(&"arch") {
256 return Some("arch".to_string());
257 }
258 if families.contains(&"fedora") {
259 return Some("fedora".to_string());
260 }
261 if families.contains(&"rhel") {
262 return Some("fedora".to_string());
263 }
264 if families.contains(&"suse") {
265 return Some("suse".to_string());
266 }
267 if families.contains(&"gentoo") {
268 return Some("gentoo".to_string());
269 }
270 }
271 if let Some(id) = info.get("ID") {
272 return match id.as_str() {
273 "debian" | "ubuntu" | "linuxmint" | "pop" | "kali" | "kubuntu" | "lubuntu"
274 | "xubuntu" | "zorin" | "elementary" => Some("debian".to_string()),
275 "arch" | "manjaro" | "cachyos" | "endeavouros" | "garuda" => {
276 Some("arch".to_string())
277 }
278 "fedora" | "centos" | "rhel" | "rocky" | "almalinux" => Some("fedora".to_string()),
279 "opensuse" | "opensuse-tumbleweed" | "opensuse-leap" => Some("suse".to_string()),
280 "gentoo" => Some("gentoo".to_string()),
281 "alpine" => Some("alpine".to_string()),
282 "void" => Some("void".to_string()),
283 "solus" => Some("solus".to_string()),
284 "guix" => Some("guix".to_string()),
285 _ => None,
286 };
287 }
288 }
289 None
290}
291
292pub fn get_linux_distribution() -> Option<String> {
293 get_linux_distribution_info().and_then(|info| info.get("ID").cloned())
294}
295
296pub fn get_native_package_manager() -> Option<String> {
297 let os = std::env::consts::OS;
298 match os {
299 "linux" => get_linux_distro_family()
300 .map(|family| {
301 match family.as_str() {
302 "debian" => "apt",
303 "arch" => "pacman",
304 "fedora" => "dnf",
305 "suse" => "zypper",
306 "gentoo" => "portage",
307 "alpine" => "apk",
308 "void" => "xbps-install",
309 "solus" => "eopkg",
310 "guix" => "guix",
311 _ => "unknown",
312 }
313 .to_string()
314 })
315 .filter(|s| s != "unknown"),
316 "macos" => {
317 if command_exists("brew") {
318 Some("brew".to_string())
319 } else if command_exists("port") {
320 Some("macports".to_string())
321 } else {
322 None
323 }
324 }
325 "windows" => {
326 if command_exists("scoop") {
327 Some("scoop".to_string())
328 } else if command_exists("choco") {
329 Some("choco".to_string())
330 } else if command_exists("winget") {
331 Some("winget".to_string())
332 } else {
333 None
334 }
335 }
336 "freebsd" => Some("pkg".to_string()),
337 "openbsd" => Some("pkg_add".to_string()),
338 _ => None,
339 }
340}
341
342pub fn print_repo_warning(repo_name: &str) {
343 if let Ok(db_path) = crate::pkg::resolve::get_db_root()
344 && let Ok(repo_config) = crate::pkg::config::read_repo_config(&db_path)
345 {
346 let major_repo = repo_name.split('/').next().unwrap_or_default();
347 if let Some(repo_entry) = repo_config.repos.iter().find(|r| r.name == major_repo) {
348 let warning_message = match repo_entry.repo_type.as_str() {
349 "unoffical" => {
350 Some("This package is from an unofficial repository and is not trusted.")
351 }
352 "community" => {
353 Some("This package is from a community repository. Use with caution.")
354 }
355 "test" => Some(
356 "This package is from a testing repository and may not function correctly.",
357 ),
358 "archive" => {
359 Some("This package is from an archive repository and is no longer maintained.")
360 }
361 _ => None,
362 };
363
364 if let Some(message) = warning_message {
365 println!("\n{}: {}", "NOTE".yellow().bold(), message.yellow());
366 }
367 }
368 }
369}
370
371pub fn confirm_untrusted_source(source_type: &SourceType, yes: bool) -> anyhow::Result<()> {
372 if source_type == &SourceType::OfficialRepo {
373 return Ok(());
374 }
375
376 let warning_message = match source_type {
377 SourceType::UntrustedRepo(repo) => {
378 format!(
379 "The package from repository '@{}' is not an official Zoi repository.",
380 repo
381 )
382 }
383 SourceType::LocalFile => "You are installing from a local file.".to_string(),
384 SourceType::Url => "You are installing from a remote URL.".to_string(),
385 _ => return Ok(()),
386 };
387
388 println!(
389 "\n{}: {}",
390 "SECURITY WARNING".yellow().bold(),
391 warning_message
392 );
393
394 if ask_for_confirmation(
395 "This source is not trusted. Are you sure you want to continue?",
396 yes,
397 ) {
398 Ok(())
399 } else {
400 Err(anyhow!("Operation aborted by user."))
401 }
402}
403
404pub fn is_platform_compatible(current_platform: &str, allowed_platforms: &[String]) -> bool {
405 let os = match std::env::consts::OS {
406 "darwin" => "macos",
407 other => other,
408 };
409 allowed_platforms
410 .iter()
411 .any(|p| p == "all" || p == os || p == current_platform)
412}
413
414pub fn setup_path(scope: Scope) -> anyhow::Result<()> {
415 if scope == Scope::Project {
416 return Ok(());
417 }
418
419 let zoi_bin_dir = match scope {
420 Scope::User => home::home_dir()
421 .ok_or_else(|| anyhow!("Could not find home directory."))?
422 .join(".zoi")
423 .join("pkgs")
424 .join("bin"),
425 Scope::System => {
426 if cfg!(target_os = "windows") {
427 PathBuf::from("C:\\ProgramData\\zoi\\pkgs\\bin")
428 } else {
429 PathBuf::from("/usr/local/bin")
430 }
431 }
432 Scope::Project => return Ok(()),
433 };
434
435 if !zoi_bin_dir.exists() {
436 fs::create_dir_all(&zoi_bin_dir)?;
437 }
438
439 if scope == Scope::System {
440 println!(
441 "{}",
442 "System-wide installation complete. Binaries are in the system PATH.".green()
443 );
444 return Ok(());
445 }
446
447 println!("{}", "Ensuring Zoi bin directory is in your PATH...".bold());
448
449 #[cfg(unix)]
450 {
451 use std::fs::{File, OpenOptions};
452 let home = home::home_dir().ok_or_else(|| anyhow!("Could not find home directory."))?;
453 let zoi_bin_str = "$HOME/.zoi/pkgs/bin";
454
455 let shell_name = std::env::var("SHELL").unwrap_or_default();
456 let (profile_file_path, cmd_to_write) = if shell_name.contains("bash") {
457 let path = if cfg!(target_os = "macos") {
458 home.join(".bash_profile")
459 } else {
460 home.join(".bashrc")
461 };
462 let cmd = format!(
463 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
464 zoi_bin_str, "$PATH"
465 );
466 (path, cmd)
467 } else if shell_name.contains("zsh") {
468 let path = home.join(".zshrc");
469 let cmd = format!(
470 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
471 zoi_bin_str, "$PATH"
472 );
473 (path, cmd)
474 } else if shell_name.contains("fish") {
475 let path = home.join(".config/fish/config.fish");
476 let cmd = format!("\n# Added by Zoi\nset -gx PATH \"{}\" $PATH\n", zoi_bin_str);
477
478 (path, cmd)
479 } else if shell_name.contains("elvish") {
480 let path = home.join(".config/elvish/rc.elv");
481 let cmd = "
482# Added by Zoi
483set paths = [ ~/.zoi/pkgs/bin $paths... ]
484"
485 .to_string();
486 (path, cmd)
487 } else if shell_name.contains("csh") || shell_name.contains("tcsh") {
488 let path = home.join(".cshrc");
489 let cmd = format!(
490 "\n# Added by Zoi\nsetenv PATH=\"{}:{}\"\n",
491 zoi_bin_str, "$PATH"
492 );
493 (path, cmd)
494 } else {
495 let path = home.join(".profile");
496 let cmd = format!(
497 "\n# Added by Zoi\nexport PATH=\"{}:{}\"\n",
498 zoi_bin_str, "$PATH"
499 );
500 (path, cmd)
501 };
502
503 if !profile_file_path.exists() {
504 if let Some(parent) = profile_file_path.parent() {
505 fs::create_dir_all(parent)?;
506 }
507 File::create(&profile_file_path)?;
508 }
509
510 let content = fs::read_to_string(&profile_file_path)?;
511 if content.contains(zoi_bin_str) {
512 println!("Zoi bin directory is already in your shell's config.");
513 return Ok(());
514 }
515
516 let mut file = OpenOptions::new().append(true).open(&profile_file_path)?;
517
518 file.write_all(cmd_to_write.as_bytes())?;
519
520 println!(
521 "{} Zoi bin directory has been added to your PATH in '{}'.",
522 "Success:".green(),
523 profile_file_path.display()
524 );
525 println!(
526 "Please restart your shell or run `source {}` for the changes to take effect.",
527 profile_file_path.display()
528 );
529 }
530
531 #[cfg(windows)]
532 {
533 use winreg::RegKey;
534 use winreg::enums::*;
535
536 let zoi_bin_path_str = zoi_bin_dir
537 .to_str()
538 .ok_or_else(|| anyhow!("Invalid path string"))?;
539
540 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
541 let env = hkcu.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;
542 let current_path: String = env.get_value("Path")?;
543
544 if current_path
545 .split(';')
546 .any(|p| p.eq_ignore_ascii_case(zoi_bin_path_str))
547 {
548 println!("Zoi bin directory is already in your PATH.");
549 return Ok(());
550 }
551
552 let new_path = if current_path.is_empty() {
553 zoi_bin_path_str.to_string()
554 } else {
555 format!("{};{}", current_path, zoi_bin_path_str)
556 };
557 env.set_value("Path", &new_path)?;
558
559 println!(
560 "{} Zoi bin directory has been added to your user PATH environment variable.",
561 "Success:".green()
562 );
563 println!(
564 "Please restart your shell or log out and log back in for the changes to take effect."
565 );
566 }
567
568 Ok(())
569}
570
571pub fn check_path() {
572 if let Some(home) = home::home_dir() {
573 let zoi_bin_dir = home.join(".zoi/pkgs/bin");
574 if !zoi_bin_dir.exists() {
575 return;
576 }
577 } else {
578 return;
579 }
580
581 let command_output = if cfg!(target_os = "windows") {
582 Command::new("pwsh")
583 .arg("-Command")
584 .arg("echo $env:Path")
585 .output()
586 } else {
587 Command::new("bash").arg("-c").arg("echo $PATH").output()
588 };
589
590 let is_in_path = match command_output {
591 Ok(output) => {
592 if output.status.success() {
593 let path_var = String::from_utf8_lossy(&output.stdout);
594 path_var.contains(".zoi/pkgs/bin")
595 } else {
596 false
597 }
598 }
599 Err(_) => false,
600 };
601
602 if !is_in_path {
603 eprintln!(
604 "Please run 'zoi shell <shell>' or add it to your PATH manually for commands to be available."
605 );
606 }
607}
608
609pub fn get_platform() -> anyhow::Result<String> {
610 let os = match std::env::consts::OS {
611 "linux" => "linux",
612 "macos" | "darwin" => "macos",
613 "windows" => "windows",
614 "freebsd" => "freebsd",
615 "openbsd" => "openbsd",
616 unsupported_os => return Err(anyhow!("Unsupported operating system: {}", unsupported_os)),
617 };
618
619 let arch = match std::env::consts::ARCH {
620 "x86_64" => "amd64",
621 "aarch64" => "arm64",
622 unsupported_arch => return Err(anyhow!("Unsupported architecture: {}", unsupported_arch)),
623 };
624
625 Ok(format!("{}-{}", os, arch))
626}
627
628pub fn get_all_available_package_managers() -> Vec<String> {
629 let mut managers = Vec::new();
630 let all_possible_managers = [
631 "apt",
632 "pacman",
633 "yay",
634 "paru",
635 "pikaur",
636 "trizen",
637 "dnf",
638 "yum",
639 "zypper",
640 "portage",
641 "apk",
642 "snap",
643 "flatpak",
644 "nix",
645 "brew",
646 "port",
647 "scoop",
648 "choco",
649 "winget",
650 "pkg",
651 "pkg_add",
652 "xbps-install",
653 "eopkg",
654 "guix",
655 "mas",
656 ];
657
658 for manager in &all_possible_managers {
659 if command_exists(manager) {
660 managers.push(manager.to_string());
661 }
662 }
663 managers.sort();
664 managers.dedup();
665 managers
666}
667
668pub fn build_blocking_http_client(timeout_secs: u64) -> anyhow::Result<reqwest::blocking::Client> {
669 let client = reqwest::blocking::Client::builder()
670 .timeout(Duration::from_secs(timeout_secs))
671 .build()?;
672 Ok(client)
673}
674
675pub fn retry_backoff_sleep(attempt: u32) {
676 let base_ms = 500u64.saturating_mul(1u64 << (attempt.saturating_sub(1)));
677 let jitter = (std::time::SystemTime::now()
678 .duration_since(std::time::UNIX_EPOCH)
679 .unwrap_or(Duration::from_secs(0))
680 .subsec_millis()
681 % 200) as u64;
682 let sleep_ms = (base_ms + jitter).min(8000);
683 std::thread::sleep(Duration::from_millis(sleep_ms));
684}
685
686pub fn check_license(license: &str) {
687 if license.is_empty() {
688 println!(
689 "{}",
690 "Warning: Package does not have a license specified.".yellow()
691 );
692 return;
693 }
694
695 if license.eq_ignore_ascii_case("Proprietary") {
696 println!(
697 "{}",
698 "Warning: Package is using a proprietary license.".red()
699 );
700 return;
701 }
702
703 if license.eq_ignore_ascii_case("Unkown") {
704 println!("{}", "Warning: Package license is unkown.".red());
705 return;
706 }
707
708 match spdx::Expression::parse(license) {
709 Ok(expr) => {
710 if !expr.evaluate(|req| match req.license {
711 spdx::LicenseItem::Spdx { id, .. } => id.is_osi_approved(),
712 spdx::LicenseItem::Other { .. } => false,
713 }) {
714 println!(
715 "{}{}{}",
716 "Warning: License '".yellow(),
717 license.yellow().bold(),
718 "' is not an OSI approved license.".yellow()
719 );
720 }
721 }
722 Err(_) => {
723 println!(
724 "{}{}{}",
725 "Warning: Could not parse license expression '".yellow(),
726 license.yellow().bold(),
727 "' It may not be a valid SPDX identifier.".yellow()
728 );
729 }
730 }
731}
732
733#[derive(serde::Deserialize)]
734struct PackageForCompletion {
735 description: Option<String>,
736}
737
738pub struct PackageCompletion {
739 pub display: String,
740 pub repo: String,
741 pub description: String,
742}
743
744pub fn get_all_packages_for_completion() -> Vec<PackageCompletion> {
745 let db_root = if let Ok(path) = crate::pkg::resolve::get_db_root() {
746 path
747 } else {
748 return Vec::new();
749 };
750
751 let active_repos = if let Ok(config) = crate::pkg::config::read_config() {
752 config.repos
753 } else {
754 return Vec::new();
755 };
756
757 if !db_root.exists() {
758 return Vec::new();
759 }
760
761 let mut packages = Vec::new();
762 for repo_name in &active_repos {
763 let repo_path = db_root.join(repo_name);
764 if !repo_path.is_dir() {
765 continue;
766 }
767 for entry in WalkDir::new(&repo_path)
768 .into_iter()
769 .filter_map(|e| e.ok())
770 .filter(|e| e.file_type().is_dir())
771 {
772 let pkg_name = entry.file_name().to_string_lossy();
773 let pkg_file_path = entry.path().join(format!("{}.pkg.lua", pkg_name));
774
775 if pkg_file_path.is_file() {
776 let pkg_info: anyhow::Result<PackageForCompletion> = (|| -> anyhow::Result<_> {
777 let pkg = crate::pkg::lua::parser::parse_lua_package(
778 pkg_file_path.to_str().unwrap(),
779 None,
780 true,
781 )?;
782 Ok(PackageForCompletion {
783 description: Some(pkg.description),
784 })
785 })();
786
787 let description = match pkg_info {
788 Ok(pi) => pi.description.unwrap_or_default(),
789 Err(_) => String::new(),
790 };
791
792 let relative_path = entry.path().strip_prefix(&db_root).unwrap();
793 let full_pkg_id =
794 format!("@{}", relative_path.to_string_lossy().replace('\\', "/"));
795
796 packages.push(PackageCompletion {
797 display: full_pkg_id,
798 repo: repo_name.clone(),
799 description,
800 });
801 }
802 }
803 }
804 packages.sort_by(|a, b| a.display.cmp(&b.display));
805 packages
806}
807
808pub fn get_current_shell() -> Option<Shell> {
809 if cfg!(windows) {
810 return Some(Shell::PowerShell);
811 }
812
813 if let Ok(shell_path) = std::env::var("SHELL") {
814 let shell_name = Path::new(&shell_path).file_name()?.to_str()?;
815 match shell_name {
816 "bash" => Some(Shell::Bash),
817 "zsh" => Some(Shell::Zsh),
818 "fish" => Some(Shell::Fish),
819 "elvish" => Some(Shell::Elvish),
820 "pwsh" => Some(Shell::PowerShell),
821 _ => None,
822 }
823 } else {
824 None
825 }
826}