Skip to main content

starbase_shell/shells/
fish.rs

1use super::Shell;
2use crate::helpers::{ProfileSet, get_config_dir, normalize_newlines};
3use crate::hooks::*;
4use crate::quoter::*;
5use shell_quote::{Fish as FishQuoter, Quotable, QuoteRefExt};
6use std::fmt;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10#[derive(Clone, Copy, Debug)]
11pub struct Fish;
12
13impl Fish {
14    #[allow(clippy::new_without_default)]
15    pub fn new() -> Self {
16        Self
17    }
18}
19
20// https://fishshell.com/docs/current/language.html#configuration
21impl Shell for Fish {
22    fn create_quoter<'a>(&self, data: Quotable<'a>) -> Quoter<'a> {
23        Quoter::new(
24            data,
25            QuoterOptions {
26                on_quote: Some(Arc::new(|data| data.quoted(FishQuoter))),
27                ..Default::default()
28            },
29        )
30    }
31
32    fn format(&self, statement: Statement<'_>) -> String {
33        match statement {
34            Statement::ModifyPath {
35                paths,
36                key,
37                orig_key,
38            } => {
39                let key = key.unwrap_or("PATH");
40                let value = paths
41                    .iter()
42                    .map(|p| format!(r#""{p}""#))
43                    .collect::<Vec<_>>()
44                    .join(" ");
45
46                match orig_key {
47                    Some(orig_key) => format!("set -gx {key} {value} ${orig_key};"),
48                    None => format!("set -gx {key} {value};"),
49                }
50            }
51            Statement::SetEnv { key, value } => {
52                format!("set -gx {} {};", key, self.quote(value))
53            }
54            Statement::UnsetEnv { key } => {
55                format!("set -ge {key};")
56            }
57        }
58    }
59
60    fn format_hook(&self, hook: Hook) -> Result<String, crate::ShellError> {
61        Ok(normalize_newlines(match hook {
62            Hook::OnChangeDir { command, function } => {
63                format!(
64                    r#"
65set -gx __ORIG_PATH $PATH
66
67function {function} --on-variable PWD;
68  {command} | source
69end;
70"#
71                )
72            }
73        }))
74    }
75
76    fn get_config_path(&self, home_dir: &Path) -> PathBuf {
77        get_config_dir(home_dir).join("fish").join("config.fish")
78    }
79
80    fn get_env_path(&self, home_dir: &Path) -> PathBuf {
81        self.get_config_path(home_dir)
82    }
83
84    fn get_profile_paths(&self, home_dir: &Path) -> Vec<PathBuf> {
85        ProfileSet::default()
86            .insert(get_config_dir(home_dir).join("fish").join("config.fish"), 1)
87            .insert(home_dir.join(".config").join("fish").join("config.fish"), 2)
88            .into_list()
89    }
90}
91
92impl fmt::Display for Fish {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "fish")
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use starbase_sandbox::assert_snapshot;
102
103    #[test]
104    fn formats_env_var() {
105        assert_eq!(
106            Fish.format_env_set("PROTO_HOME", "$HOME/.proto"),
107            r#"set -gx PROTO_HOME "$HOME/.proto";"#
108        );
109    }
110
111    #[test]
112    fn formats_path_prepend() {
113        assert_eq!(
114            Fish.format_path_prepend(&["$PROTO_HOME/shims".into(), "$PROTO_HOME/bin".into()]),
115            r#"set -gx PATH "$PROTO_HOME/shims" "$PROTO_HOME/bin" $PATH;"#
116        );
117    }
118
119    #[test]
120    fn formats_path_set() {
121        assert_eq!(
122            Fish.format_path_set(&["$PROTO_HOME/shims".into(), "$PROTO_HOME/bin".into()]),
123            r#"set -gx PATH "$PROTO_HOME/shims" "$PROTO_HOME/bin";"#
124        );
125    }
126
127    #[test]
128    fn formats_cd_hook() {
129        let hook = Hook::OnChangeDir {
130            command: "starbase hook fish".into(),
131            function: "_starbase_hook".into(),
132        };
133
134        assert_snapshot!(Fish.format_hook(hook).unwrap());
135    }
136
137    #[test]
138    fn test_profile_paths() {
139        #[allow(deprecated)]
140        let home_dir = std::env::home_dir().unwrap();
141
142        assert_eq!(
143            Fish::new().get_profile_paths(&home_dir),
144            vec![home_dir.join(".config").join("fish").join("config.fish")]
145        );
146    }
147
148    #[test]
149    fn test_fish_quoting() {
150        // assert_eq!(Fish.quote("\n"), r#"\n"#);
151        // assert_eq!(Fish.quote("\t"), r#"\t"#);
152        // assert_eq!(Fish.quote("\x07"), r#"\a"#);
153        // assert_eq!(Fish.quote("\x08"), r#"\b"#);
154        // assert_eq!(Fish.quote("\x1b"), r#"\e"#);
155        // assert_eq!(Fish.quote("\x0c"), r#"\f"#);
156        // assert_eq!(Fish.quote("\r"), r#"\r"#);
157        // assert_eq!(Fish.quote("\x0a"), r#"\n"#);
158        // assert_eq!(Fish.quote("\x0b"), r#"\v"#);
159        // assert_eq!(Fish.quote("*"), r#""\*""#);
160        // assert_eq!(Fish.quote("?"), r#""\?""#);
161        // assert_eq!(Fish.quote("~"), r#""\~""#);
162        // assert_eq!(Fish.quote("#"), r#""\#""#);
163        // assert_eq!(Fish.quote("("), r#""\(""#);
164        // assert_eq!(Fish.quote(")"), r#""\)""#);
165        // assert_eq!(Fish.quote("{"), r#""\{""#);
166        // assert_eq!(Fish.quote("}"), r#""\}""#);
167        // assert_eq!(Fish.quote("["), r#""\[""#);
168        // assert_eq!(Fish.quote("]"), r#""\]""#);
169        // assert_eq!(Fish.quote("<"), r#""\<""#);
170        // assert_eq!(Fish.quote(">"), r#""\>""#);
171        // assert_eq!(Fish.quote("^"), r#""\^""#);
172        // assert_eq!(Fish.quote("&"), r#""\&""#);
173        // assert_eq!(Fish.quote("|"), r#""\|""#);
174        // assert_eq!(Fish.quote(";"), r#""\;""#);
175        // assert_eq!(Fish.quote("\""), r#""\"""#);
176        assert_eq!(Fish.quote("$"), "'$'");
177        assert_eq!(Fish.quote("$variable"), "\"$variable\"");
178        assert_eq!(Fish.quote("value with spaces"), "value' with spaces'");
179    }
180}