use std::ffi::OsStr;
use std::path::Path;
use std::process::Command;
use serde::Serialize;
use sysinfo::{Process, System, get_current_pid};
use thiserror::Error;
#[derive(PartialEq, derive_more::Display)]
pub enum Shell {
#[display("sh")]
Sh,
#[display("bash")]
Bash,
#[display("fish")]
Fish,
#[display("zsh")]
Zsh,
#[display("xonsh")]
Xonsh,
#[display("nu")]
Nu,
#[display("powershell")]
Powershell,
#[display("unknown")]
Unknown,
}
#[derive(Debug, Error, Serialize)]
pub enum ShellError {
#[error("shell not supported")]
NotSupported,
#[error("failed to execute shell command: {0}")]
ExecError(String),
}
impl Shell {
pub fn current() -> Self {
let sys = System::new_all();
let process = sys
.process(get_current_pid().expect("Failed to get current PID"))
.expect("Process with current pid does not exist");
let parent = sys
.process(process.parent().expect("Atuin running with no parent!"))
.expect("Process with parent pid does not exist");
let shell = parent.name().trim().to_lowercase();
let shell = shell.strip_prefix('-').unwrap_or(&shell);
Self::from_string(shell)
}
pub fn from_env() -> Self {
std::env::var("ATUIN_SHELL")
.map_or(Self::Unknown, |shell| Self::from_string(&shell.trim().to_lowercase()))
}
pub fn config_file(&self) -> Option<std::path::PathBuf> {
let mut path = directories::BaseDirs::new()?.home_dir().to_owned();
match self {
Self::Bash => path.push(".bashrc"),
Self::Zsh => path.push(".zshrc"),
Self::Fish => path.push(".config/fish/config.fish"),
_ => return None,
};
Some(path)
}
pub fn default_shell() -> Result<Self, ShellError> {
let sys = System::name().unwrap_or("".to_string()).to_lowercase();
let path = if sys.contains("darwin") {
Self::Sh.run_interactive([
"dscl localhost -read \"/Local/Default/Users/$USER\" shell | awk '{print $2}'"
])?
} else if cfg!(windows) {
return Ok(Self::Powershell);
} else {
Self::Sh.run_interactive(["getent passwd $LOGNAME | cut -d: -f7"])?
};
let path = Path::new(path.trim());
let shell = path.file_name();
if shell.is_none() {
return Err(ShellError::NotSupported);
}
Ok(Self::from_string(&shell.unwrap().to_string_lossy()))
}
pub fn from_string(name: &str) -> Self {
match name {
"bash" => Self::Bash,
"fish" => Self::Fish,
"zsh" => Self::Zsh,
"xonsh" => Self::Xonsh,
"nu" => Self::Nu,
"sh" => Self::Sh,
"powershell" => Self::Powershell,
_ => Self::Unknown,
}
}
pub fn is_posixish(&self) -> bool {
matches!(self, Self::Bash | Self::Fish | Self::Zsh)
}
pub fn run_interactive<I, S>(&self, args: I) -> Result<String, ShellError>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let shell = self.to_string();
let output = if self == &Self::Powershell {
Command::new(shell)
.args(args)
.output()
.map_err(|e| ShellError::ExecError(e.to_string()))?
} else {
Command::new(shell)
.arg("-ic")
.args(args)
.output()
.map_err(|e| ShellError::ExecError(e.to_string()))?
};
Ok(String::from_utf8(output.stdout).unwrap())
}
}
pub fn shell_name(parent: Option<&Process>) -> String {
let sys = System::new_all();
let parent = if let Some(parent) = parent {
parent
} else {
let process = sys
.process(get_current_pid().expect("Failed to get current PID"))
.expect("Process with current pid does not exist");
sys.process(process.parent().expect("Atuin running with no parent!"))
.expect("Process with parent pid does not exist")
};
let shell = parent.name().trim().to_lowercase();
let shell = shell.strip_prefix('-').unwrap_or(&shell);
shell.to_string()
}