zoi/
utils.rs

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