1use std::{ffi::OsStr, path::Path, process::Command};
2
3use serde::Serialize;
4use sysinfo::{get_current_pid, Process, System};
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 config_file(&self) -> Option<std::path::PathBuf> {
66 let mut path = if let Some(base) = directories::BaseDirs::new() {
67 base.home_dir().to_owned()
68 } else {
69 return None;
70 };
71
72 match self {
74 Shell::Bash => path.push(".bashrc"),
75 Shell::Zsh => path.push(".zshrc"),
76 Shell::Fish => path.push(".config/fish/config.fish"),
77
78 _ => return None,
79 };
80
81 Some(path)
82 }
83
84 pub fn default_shell() -> Result<Shell, ShellError> {
88 let sys = System::name().unwrap_or("".to_string()).to_lowercase();
89
90 let path = if sys.contains("darwin") {
93 Shell::Sh.run_interactive([
95 "dscl localhost -read \"/Local/Default/Users/$USER\" shell | awk '{print $2}'",
96 ])?
97 } else if cfg!(windows) {
98 return Ok(Shell::Powershell);
99 } else {
100 Shell::Sh.run_interactive(["getent passwd $LOGNAME | cut -d: -f7"])?
101 };
102
103 let path = Path::new(path.trim());
104 let shell = path.file_name();
105
106 if shell.is_none() {
107 return Err(ShellError::NotSupported);
108 }
109
110 Ok(Shell::from_string(
111 shell.unwrap().to_string_lossy().to_string(),
112 ))
113 }
114
115 pub fn from_string(name: String) -> Shell {
116 match name.as_str() {
117 "bash" => Shell::Bash,
118 "fish" => Shell::Fish,
119 "zsh" => Shell::Zsh,
120 "xonsh" => Shell::Xonsh,
121 "nu" => Shell::Nu,
122 "sh" => Shell::Sh,
123 "powershell" => Shell::Powershell,
124
125 _ => Shell::Unknown,
126 }
127 }
128
129 pub fn is_posixish(&self) -> bool {
133 matches!(self, Shell::Bash | Shell::Fish | Shell::Zsh)
134 }
135
136 pub fn run_interactive<I, S>(&self, args: I) -> Result<String, ShellError>
137 where
138 I: IntoIterator<Item = S>,
139 S: AsRef<OsStr>,
140 {
141 let shell = self.to_string();
142 let output = if self == &Self::Powershell {
143 Command::new(shell)
144 .args(args)
145 .output()
146 .map_err(|e| ShellError::ExecError(e.to_string()))?
147 } else {
148 Command::new(shell)
149 .arg("-ic")
150 .args(args)
151 .output()
152 .map_err(|e| ShellError::ExecError(e.to_string()))?
153 };
154
155 Ok(String::from_utf8(output.stdout).unwrap())
156 }
157}
158
159pub fn shell_name(parent: Option<&Process>) -> String {
160 let sys = System::new_all();
161
162 let parent = if let Some(parent) = parent {
163 parent
164 } else {
165 let process = sys
166 .process(get_current_pid().expect("Failed to get current PID"))
167 .expect("Process with current pid does not exist");
168
169 sys.process(process.parent().expect("Atuin running with no parent!"))
170 .expect("Process with parent pid does not exist")
171 };
172
173 let shell = parent.name().trim().to_lowercase();
174 let shell = shell.strip_prefix('-').unwrap_or(&shell);
175
176 shell.to_string()
177}