Skip to main content

roder_api/
command_shell.rs

1use std::path::Path;
2
3pub fn default_command_shell() -> String {
4    default_command_shell_for(
5        std::env::var("SHELL").ok().as_deref(),
6        cfg!(target_os = "macos"),
7        cfg!(target_os = "windows"),
8    )
9}
10
11pub fn normalize_command_shell(shell: &str) -> Option<String> {
12    let shell = shell.trim();
13    (!shell.is_empty()).then(|| shell.to_string())
14}
15
16pub fn command_shell_options(active: &str) -> Vec<String> {
17    let mut shells = Vec::new();
18    push_unique_shell(&mut shells, active);
19    push_unique_shell(&mut shells, "zsh");
20    push_unique_shell(&mut shells, "bash");
21    shells
22}
23
24fn push_unique_shell(shells: &mut Vec<String>, shell: &str) {
25    if let Some(shell) = normalize_command_shell(shell)
26        && !shells.iter().any(|known| known == &shell)
27    {
28        shells.push(shell);
29    }
30}
31
32fn default_command_shell_for(
33    login_shell: Option<&str>,
34    is_macos: bool,
35    is_windows: bool,
36) -> String {
37    if is_windows {
38        return "powershell".to_string();
39    }
40
41    if let Some(login_shell) = login_shell.and_then(normalize_command_shell)
42        && shell_name(&login_shell) == Some("zsh")
43    {
44        return login_shell;
45    }
46
47    if is_macos {
48        "/bin/zsh".to_string()
49    } else {
50        "bash".to_string()
51    }
52}
53
54fn shell_name(shell: &str) -> Option<&str> {
55    Path::new(shell)
56        .file_name()
57        .and_then(|name| name.to_str())
58        .or_else(|| (!shell.trim().is_empty()).then_some(shell))
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn default_prefers_zsh_login_shell() {
67        assert_eq!(
68            default_command_shell_for(Some("/usr/local/bin/zsh"), false, false),
69            "/usr/local/bin/zsh"
70        );
71    }
72
73    #[test]
74    fn default_uses_macos_zsh_when_login_shell_is_not_zsh() {
75        assert_eq!(
76            default_command_shell_for(Some("/usr/local/bin/fish"), true, false),
77            "/bin/zsh"
78        );
79    }
80
81    #[test]
82    fn default_uses_bash_off_macos_when_login_shell_is_not_zsh() {
83        assert_eq!(
84            default_command_shell_for(Some("/usr/local/bin/fish"), false, false),
85            "bash"
86        );
87    }
88
89    #[test]
90    fn default_uses_powershell_on_windows() {
91        assert_eq!(
92            default_command_shell_for(Some("/usr/local/bin/zsh"), false, true),
93            "powershell"
94        );
95    }
96
97    #[test]
98    fn command_shell_options_include_active_and_common_shells_once() {
99        assert_eq!(
100            command_shell_options("/opt/homebrew/bin/fish"),
101            vec![
102                "/opt/homebrew/bin/fish".to_string(),
103                "zsh".to_string(),
104                "bash".to_string(),
105            ]
106        );
107        assert_eq!(
108            command_shell_options("zsh"),
109            vec!["zsh".to_string(), "bash".to_string()]
110        );
111    }
112}