1use std::{ffi::OsStr, path::Path, process::Command};
2
3use serde::Serialize;
4use sysinfo::{Process, System, get_current_pid};
5use thiserror::Error;
6
7#[derive(PartialEq)]
8pub enum Shell {
9 Sh,
10 Bash,
11 Fish,
12 Zsh,
13 Xonsh,
14 Nu,
15 Powershell,
16
17 Unknown,
18}
19
20impl std::fmt::Display for Shell {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 let shell = match self {
23 Shell::Bash => "bash",
24 Shell::Fish => "fish",
25 Shell::Zsh => "zsh",
26 Shell::Nu => "nu",
27 Shell::Xonsh => "xonsh",
28 Shell::Sh => "sh",
29 Shell::Powershell => "powershell",
30
31 Shell::Unknown => "unknown",
32 };
33
34 write!(f, "{shell}")
35 }
36}
37
38#[derive(Debug, Error, Serialize)]
39pub enum ShellError {
40 #[error("shell not supported")]
41 NotSupported,
42
43 #[error("failed to execute shell command: {0}")]
44 ExecError(String),
45}
46
47impl Shell {
48 pub fn current() -> Shell {
49 let sys = System::new_all();
50
51 let process = sys
52 .process(get_current_pid().expect("Failed to get current PID"))
53 .expect("Process with current pid does not exist");
54
55 let parent = sys
56 .process(process.parent().expect("Atuin running with no parent!"))
57 .expect("Process with parent pid does not exist");
58
59 let shell = parent.name().trim().to_lowercase();
60 let shell = shell.strip_prefix('-').unwrap_or(&shell);
61
62 Shell::from_string(shell.to_string())
63 }
64
65 pub fn from_env() -> Shell {
66 std::env::var("ATUIN_SHELL").map_or(Shell::Unknown, |shell| {
67 Shell::from_string(shell.trim().to_lowercase())
68 })
69 }
70
71 pub fn config_file(&self) -> Option<std::path::PathBuf> {
72 let mut path = if let Some(base) = directories::BaseDirs::new() {
73 base.home_dir().to_owned()
74 } else {
75 return None;
76 };
77
78 match self {
80 Shell::Bash => path.push(".bashrc"),
81 Shell::Zsh => path.push(".zshrc"),
82 Shell::Fish => path.push(".config/fish/config.fish"),
83
84 _ => return None,
85 };
86
87 Some(path)
88 }
89
90 pub fn default_shell() -> Result<Shell, ShellError> {
94 let sys = System::name().unwrap_or("".to_string()).to_lowercase();
95
96 let path = if sys.contains("darwin") {
99 Shell::Sh.run_interactive([
101 "dscl localhost -read \"/Local/Default/Users/$USER\" shell | awk '{print $2}'",
102 ])?
103 } else if cfg!(windows) {
104 return Ok(Shell::Powershell);
105 } else {
106 Shell::Sh.run_interactive(["getent passwd $LOGNAME | cut -d: -f7"])?
107 };
108
109 let path = Path::new(path.trim());
110 let shell = path.file_name();
111
112 if shell.is_none() {
113 return Err(ShellError::NotSupported);
114 }
115
116 Ok(Shell::from_string(
117 shell.unwrap().to_string_lossy().to_string(),
118 ))
119 }
120
121 pub fn from_string(name: String) -> Shell {
122 match name.as_str() {
123 "bash" => Shell::Bash,
124 "fish" => Shell::Fish,
125 "zsh" => Shell::Zsh,
126 "xonsh" => Shell::Xonsh,
127 "nu" => Shell::Nu,
128 "sh" => Shell::Sh,
129 "powershell" => Shell::Powershell,
130
131 _ => Shell::Unknown,
132 }
133 }
134
135 pub fn is_posixish(&self) -> bool {
139 matches!(self, Shell::Bash | Shell::Fish | Shell::Zsh)
140 }
141
142 pub fn run_interactive<I, S>(&self, args: I) -> Result<String, ShellError>
143 where
144 I: IntoIterator<Item = S>,
145 S: AsRef<OsStr>,
146 {
147 let shell = self.to_string();
148 let output = if self == &Self::Powershell {
149 Command::new(shell)
150 .args(args)
151 .output()
152 .map_err(|e| ShellError::ExecError(e.to_string()))?
153 } else {
154 Command::new(shell)
155 .arg("-ic")
156 .args(args)
157 .output()
158 .map_err(|e| ShellError::ExecError(e.to_string()))?
159 };
160
161 Ok(String::from_utf8(output.stdout).unwrap())
162 }
163}
164
165pub fn shell_name(parent: Option<&Process>) -> String {
166 let sys = System::new_all();
167
168 let parent = if let Some(parent) = parent {
169 parent
170 } else {
171 let process = sys
172 .process(get_current_pid().expect("Failed to get current PID"))
173 .expect("Process with current pid does not exist");
174
175 sys.process(process.parent().expect("Atuin running with no parent!"))
176 .expect("Process with parent pid does not exist")
177 };
178
179 let shell = parent.name().trim().to_lowercase();
180 let shell = shell.strip_prefix('-').unwrap_or(&shell);
181
182 shell.to_string()
183}