starship/modules/
nix_shell.rs

1use super::{Context, Module, ModuleConfig};
2
3use crate::configs::nix_shell::NixShellConfig;
4use crate::formatter::StringFormatter;
5
6enum NixShellType {
7    Pure,
8    Impure,
9    /// We're in a Nix shell, but we don't know which type.
10    /// This can only happen in a `nix shell` shell (not a `nix-shell` one).
11    Unknown,
12}
13
14impl NixShellType {
15    fn detect_shell_type(use_heuristic: bool, context: &Context) -> Option<Self> {
16        use NixShellType::{Impure, Pure, Unknown};
17
18        let shell_type = context.get_env("IN_NIX_SHELL");
19        match shell_type.as_deref() {
20            Some("pure") => return Some(Pure),
21            Some("impure") => return Some(Impure),
22            _ => {}
23        }
24
25        if use_heuristic {
26            Self::in_new_nix_shell(context).map(|()| Unknown)
27        } else {
28            None
29        }
30    }
31
32    // Hack to detect if we're in a `nix shell` (in contrast to a `nix-shell`).
33    // A better way to do this will be enabled by https://github.com/NixOS/nix/issues/6677.
34    fn in_new_nix_shell(context: &Context) -> Option<()> {
35        let path = context.get_env("PATH")?;
36
37        std::env::split_paths(&path)
38            .any(|path| path.starts_with("/nix/store"))
39            .then_some(())
40    }
41}
42
43/// Creates a module showing if inside a nix-shell
44///
45/// The module will use the `$IN_NIX_SHELL` and `$name` environment variable to
46/// determine if it's inside a nix-shell and the name of it.
47///
48/// The following options are available:
49///     - `impure_msg` (string)  // change the impure msg
50///     - `pure_msg` (string)    // change the pure msg
51///     - `unknown_msg` (string) // change the unknown message
52///
53/// Will display the following:
54///     - pure (name)    // $name == "name" in a pure nix-shell
55///     - impure (name)  // $name == "name" in an impure nix-shell
56///     - pure           // $name == "" in a pure nix-shell
57///     - impure         // $name == "" in an impure nix-shell
58///     - unknown (name) // $name == "name" in an unknown nix-shell
59///     - unknown        // $name == "" in an unknown nix-shell
60pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
61    let mut module = context.new_module("nix_shell");
62    let config: NixShellConfig = NixShellConfig::try_load(module.config);
63
64    let shell_name = context.get_env("name");
65    let shell_type = NixShellType::detect_shell_type(config.heuristic, context)?;
66    let shell_type_format = match shell_type {
67        NixShellType::Pure => config.pure_msg,
68        NixShellType::Impure => config.impure_msg,
69        NixShellType::Unknown => config.unknown_msg,
70    };
71
72    let parsed = StringFormatter::new(config.format).and_then(|formatter| {
73        formatter
74            .map_meta(|variable, _| match variable {
75                "symbol" => Some(config.symbol),
76                "state" => Some(shell_type_format),
77                _ => None,
78            })
79            .map_style(|variable| match variable {
80                "style" => Some(Ok(config.style)),
81                _ => None,
82            })
83            .map(|variable| match variable {
84                "name" => shell_name.as_ref().map(Ok),
85                _ => None,
86            })
87            .parse(None, Some(context))
88    });
89
90    module.set_segments(match parsed {
91        Ok(segments) => segments,
92        Err(error) => {
93            log::warn!("Error in module `nix_shell`:\n{error}");
94            return None;
95        }
96    });
97
98    Some(module)
99}
100
101#[cfg(test)]
102mod tests {
103    use crate::test::ModuleRenderer;
104    use nu_ansi_term::Color;
105
106    #[test]
107    fn no_env_variables() {
108        let actual = ModuleRenderer::new("nix_shell").collect();
109        let expected = None;
110
111        assert_eq!(expected, actual);
112    }
113
114    #[test]
115    fn invalid_env_variables() {
116        let actual = ModuleRenderer::new("nix_shell")
117            .env("IN_NIX_SHELL", "something_wrong")
118            .collect();
119        let expected = None;
120
121        assert_eq!(expected, actual);
122    }
123
124    #[test]
125    fn pure_shell() {
126        let actual = ModuleRenderer::new("nix_shell")
127            .env("IN_NIX_SHELL", "pure")
128            .collect();
129        let expected = Some(format!("via {} ", Color::Blue.bold().paint("❄️  pure")));
130
131        assert_eq!(expected, actual);
132    }
133
134    #[test]
135    fn impure_shell() {
136        let actual = ModuleRenderer::new("nix_shell")
137            .env("IN_NIX_SHELL", "impure")
138            .collect();
139        let expected = Some(format!("via {} ", Color::Blue.bold().paint("❄️  impure")));
140
141        assert_eq!(expected, actual);
142    }
143
144    #[test]
145    fn pure_shell_name() {
146        let actual = ModuleRenderer::new("nix_shell")
147            .env("IN_NIX_SHELL", "pure")
148            .env("name", "starship")
149            .collect();
150        let expected = Some(format!(
151            "via {} ",
152            Color::Blue.bold().paint("❄️  pure (starship)")
153        ));
154
155        assert_eq!(expected, actual);
156    }
157
158    #[test]
159    fn impure_shell_name() {
160        let actual = ModuleRenderer::new("nix_shell")
161            .env("IN_NIX_SHELL", "impure")
162            .env("name", "starship")
163            .collect();
164        let expected = Some(format!(
165            "via {} ",
166            Color::Blue.bold().paint("❄️  impure (starship)")
167        ));
168
169        assert_eq!(expected, actual);
170    }
171
172    #[test]
173    fn new_nix_shell() {
174        let actual = ModuleRenderer::new("nix_shell")
175            .env(
176                "PATH",
177                "/nix/store/v7qvqv81jp0cajvrxr9x072jgqc01yhi-nix-info/bin:/Users/user/.cargo/bin",
178            )
179            .config(toml::toml! {
180                [nix_shell]
181                heuristic = true
182            })
183            .collect();
184        let expected = Some(format!("via {} ", Color::Blue.bold().paint("❄️  ")));
185
186        assert_eq!(expected, actual);
187    }
188
189    #[test]
190    fn no_new_nix_shell() {
191        let actual = ModuleRenderer::new("nix_shell")
192            .env("PATH", "/Users/user/.cargo/bin")
193            .config(toml::toml! {
194                [nix_shell]
195                heuristic = true
196            })
197            .collect();
198        let expected = None;
199
200        assert_eq!(expected, actual);
201    }
202
203    #[test]
204    fn no_new_nix_shell_with_nix_store_subdirectory() {
205        let actual = ModuleRenderer::new("nix_shell")
206            .env("PATH", "/Users/user/some/nix/store/subdirectory")
207            .config(toml::toml! {
208                [nix_shell]
209                heuristic = true
210            })
211            .collect();
212        let expected = None;
213
214        assert_eq!(expected, actual);
215    }
216
217    #[test]
218    fn no_new_nix_shell_when_heuristic_is_disabled() {
219        let actual = ModuleRenderer::new("nix_shell")
220            .env(
221                "PATH",
222                "/nix/store/v7qvqv81jp0cajvrxr9x072jgqc01yhi-nix-info/bin:/Users/user/.cargo/bin",
223            )
224            .collect();
225        let expected = None;
226
227        assert_eq!(expected, actual);
228    }
229}