1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
use std::{fmt::Display, path::Path};

use indoc::formatdoc;

use crate::shell::{is_dir_in_path, Shell};

#[derive(Default)]
pub struct Nushell {}

enum EnvOp<'a> {
    Set { key: &'a str, val: &'a str },
    Hide { key: &'a str },
}

impl<'a> Display for EnvOp<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        #[allow(clippy::write_with_newline)]
        match self {
            EnvOp::Set { key, val } => write!(f, "set,{key},{val}\n"),
            EnvOp::Hide { key } => write!(f, "hide,{key},\n"),
        }
    }
}

impl Shell for Nushell {
    fn activate(&self, exe: &Path, status: bool) -> String {
        let dir = exe.parent().unwrap();
        let exe = exe.display();
        let status = if status { " --status" } else { "" };
        let mut out = String::new();

        if !is_dir_in_path(dir) {
            out.push_str(&format!(
                "let-env PATH = ($env.PATH | prepend '{}')\n", // TODO: set PATH as Path on windows
                dir.display()
            ));
        }

        out.push_str(&formatdoc! {r#"
          export-env {{
            let-env RTX_SHELL = "nu"
            
            let-env config = ($env.config | upsert hooks {{
                pre_prompt: [{{
                condition: {{ "RTX_SHELL" in $env }}
                code: {{ rtx_hook }}
                }}]
                env_change: {{
                    PWD: [{{
                    condition: {{ "RTX_SHELL" in $env }}
                    code: {{ rtx_hook }}
                    }}]
                }}
            }})
          }}
            
          def "parse vars" [] {{
            $in | lines | parse "{{op}},{{name}},{{value}}"
          }}
            
          def-env rtx [command?: string, --help, ...rest: string] {{
            let commands = ["shell", "deactivate"]
            
            if ($command == null) {{
              ^"{exe}"
            }} else if ($command == "activate") {{
              let-env RTX_SHELL = "nu"
            }} else if ($command in $commands) {{
              ^"{exe}" $command $rest
              | parse vars
              | update-env
            }} else {{
              ^"{exe}" {exe} $command $rest
            }}
          }}
            
          def-env "update-env" [] {{
            for $var in $in {{
              if $var.op == "set" {{
                let-env $var.name = $"($var.value)"
              }} else if $var.op == "hide" {{
                hide-env $var.name
              }}
            }}
          }}
            
          def-env rtx_hook [] {{
            ^"{exe}" hook-env{status} -s nu
              | parse vars
              | update-env
          }}

        "#});

        out
    }

    fn deactivate(&self) -> String {
        self.unset_env("RTX_SHELL")
    }

    fn set_env(&self, k: &str, v: &str) -> String {
        let k = shell_escape::unix::escape(k.into());
        let v = shell_escape::unix::escape(v.into());
        let v = v.replace("\\n", "\n");
        let v = v.replace('\'', "");

        EnvOp::Set { key: &k, val: &v }.to_string()
    }

    fn unset_env(&self, k: &str) -> String {
        let k = shell_escape::unix::escape(k.into());
        EnvOp::Hide { key: k.as_ref() }.to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test::replace_path;
    use insta::assert_snapshot;

    #[test]
    fn test_hook_init() {
        let nushell = Nushell::default();
        let exe = Path::new("/some/dir/rtx");
        assert_snapshot!(nushell.activate(exe, true));
    }

    #[test]
    fn test_set_env() {
        assert_snapshot!(Nushell::default().set_env("FOO", "1"));
    }

    #[test]
    fn test_unset_env() {
        assert_snapshot!(Nushell::default().unset_env("FOO"));
    }

    #[test]
    fn test_deactivate() {
        let deactivate = Nushell::default().deactivate();
        assert_snapshot!(replace_path(&deactivate));
    }
}