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 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}