Skip to main content

frm/
shell.rs

1// Copyright (c) 2025-2026 Michael S. Klishin and Contributors
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::env;
10use std::fmt;
11use std::str::FromStr;
12
13use clap::ValueEnum;
14
15use crate::common::env_vars::FRM_SHELL;
16use crate::errors::Error;
17use crate::paths::Paths;
18use crate::version::Version;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
21pub enum Shell {
22    Bash,
23    Zsh,
24    Nu,
25}
26
27const ENV_BASH_TEMPLATE: &str = include_str!("../shells/env/bash.template");
28const ENV_ZSH_TEMPLATE: &str = include_str!("../shells/env/zsh.template");
29const ENV_NU_TEMPLATE: &str = include_str!("../shells/env/nu.template");
30
31const INIT_BASH_TEMPLATE: &str = include_str!("../shells/init/bash.template");
32const INIT_ZSH_TEMPLATE: &str = include_str!("../shells/init/zsh.template");
33const INIT_NU_TEMPLATE: &str = include_str!("../shells/init/nu.template");
34
35impl Shell {
36    pub fn detect() -> Option<Self> {
37        if let Ok(shell) = env::var(FRM_SHELL) {
38            return shell.parse().ok();
39        }
40
41        if env::var("NU_VERSION").is_ok() {
42            return Some(Shell::Nu);
43        }
44
45        env::var("SHELL").ok().and_then(|s| {
46            if s.contains("bash") {
47                Some(Shell::Bash)
48            } else if s.contains("zsh") {
49                Some(Shell::Zsh)
50            } else if s.contains("nu") {
51                Some(Shell::Nu)
52            } else {
53                None
54            }
55        })
56    }
57
58    pub fn env_script(&self, paths: &Paths, version: &Version) -> String {
59        let sbin_path = paths.version_sbin_dir(version).display().to_string();
60        let base_dir = paths.base_dir().display().to_string();
61        let version_dir = paths.version_dir(version).display().to_string();
62
63        let template = match self {
64            Shell::Bash => ENV_BASH_TEMPLATE,
65            Shell::Zsh => ENV_ZSH_TEMPLATE,
66            Shell::Nu => ENV_NU_TEMPLATE,
67        };
68
69        template
70            .replace("{{sbin_path}}", &sbin_path)
71            .replace("{{base_dir}}", &base_dir)
72            .replace("{{version_dir}}", &version_dir)
73    }
74
75    pub fn init_script(&self, paths: &Paths) -> String {
76        let base_dir = paths.base_dir().display().to_string();
77
78        let template = match self {
79            Shell::Bash => INIT_BASH_TEMPLATE,
80            Shell::Zsh => INIT_ZSH_TEMPLATE,
81            Shell::Nu => INIT_NU_TEMPLATE,
82        };
83
84        template.replace("{{base_dir}}", &base_dir)
85    }
86}
87
88impl fmt::Display for Shell {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Shell::Bash => write!(f, "bash"),
92            Shell::Zsh => write!(f, "zsh"),
93            Shell::Nu => write!(f, "nu"),
94        }
95    }
96}
97
98impl FromStr for Shell {
99    type Err = Error;
100
101    fn from_str(s: &str) -> Result<Self, Self::Err> {
102        match s.to_lowercase().as_str() {
103            "bash" => Ok(Shell::Bash),
104            "zsh" => Ok(Shell::Zsh),
105            "nu" | "nushell" => Ok(Shell::Nu),
106            _ => Err(Error::Config(format!("unsupported shell: {}", s))),
107        }
108    }
109}