starbase_shell/shells/
fish.rs1use 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
20impl 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("$"), "'$'");
177 assert_eq!(Fish.quote("$variable"), "\"$variable\"");
178 assert_eq!(Fish.quote("value with spaces"), "value' with spaces'");
179 }
180}