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::errors::Error;
16use crate::paths::Paths;
17use crate::version::Version;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
20pub enum Shell {
21    Bash,
22    Zsh,
23    Nu,
24}
25
26const ENV_BASH_TEMPLATE: &str = include_str!("../shells/env/bash.template");
27const ENV_ZSH_TEMPLATE: &str = include_str!("../shells/env/zsh.template");
28const ENV_NU_TEMPLATE: &str = include_str!("../shells/env/nu.template");
29
30const INIT_BASH_TEMPLATE: &str = include_str!("../shells/init/bash.template");
31const INIT_ZSH_TEMPLATE: &str = include_str!("../shells/init/zsh.template");
32const INIT_NU_TEMPLATE: &str = include_str!("../shells/init/nu.template");
33
34impl Shell {
35    pub fn detect() -> Option<Self> {
36        if let Ok(shell) = env::var("FRM_SHELL") {
37            return shell.parse().ok();
38        }
39
40        if env::var("NU_VERSION").is_ok() {
41            return Some(Shell::Nu);
42        }
43
44        env::var("SHELL").ok().and_then(|s| {
45            if s.contains("bash") {
46                Some(Shell::Bash)
47            } else if s.contains("zsh") {
48                Some(Shell::Zsh)
49            } else if s.contains("nu") {
50                Some(Shell::Nu)
51            } else {
52                None
53            }
54        })
55    }
56
57    pub fn env_script(&self, paths: &Paths, version: &Version) -> String {
58        let sbin_path = paths.version_sbin_dir(version).display().to_string();
59        let base_dir = paths.base_dir().display().to_string();
60        let version_dir = paths.version_dir(version).display().to_string();
61
62        let template = match self {
63            Shell::Bash => ENV_BASH_TEMPLATE,
64            Shell::Zsh => ENV_ZSH_TEMPLATE,
65            Shell::Nu => ENV_NU_TEMPLATE,
66        };
67
68        template
69            .replace("{{sbin_path}}", &sbin_path)
70            .replace("{{base_dir}}", &base_dir)
71            .replace("{{version_dir}}", &version_dir)
72    }
73
74    pub fn init_script(&self, paths: &Paths) -> String {
75        let base_dir = paths.base_dir().display().to_string();
76
77        let template = match self {
78            Shell::Bash => INIT_BASH_TEMPLATE,
79            Shell::Zsh => INIT_ZSH_TEMPLATE,
80            Shell::Nu => INIT_NU_TEMPLATE,
81        };
82
83        template.replace("{{base_dir}}", &base_dir)
84    }
85}
86
87impl fmt::Display for Shell {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            Shell::Bash => write!(f, "bash"),
91            Shell::Zsh => write!(f, "zsh"),
92            Shell::Nu => write!(f, "nu"),
93        }
94    }
95}
96
97impl FromStr for Shell {
98    type Err = Error;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        match s.to_lowercase().as_str() {
102            "bash" => Ok(Shell::Bash),
103            "zsh" => Ok(Shell::Zsh),
104            "nu" | "nushell" => Ok(Shell::Nu),
105            _ => Err(Error::Config(format!("unsupported shell: {}", s))),
106        }
107    }
108}