use std::error::Error;
use std::fmt;
use crate::formatdoc;
use crate::shell::{Script, Shell};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hook {
bin: String,
subcommand: String,
variable: String,
}
impl Hook {
pub fn new(bin: &str, subcommand: &str, variable: &str) -> Result<Self, HookError> {
if !is_command_word(bin) {
return Err(HookError::Bin(bin.to_owned()));
}
if !is_command_word(subcommand) {
return Err(HookError::Subcommand(subcommand.to_owned()));
}
if !is_variable_name(variable) {
return Err(HookError::Variable(variable.to_owned()));
}
Ok(Self {
bin: bin.to_owned(),
subcommand: subcommand.to_owned(),
variable: variable.to_owned(),
})
}
#[must_use]
pub fn script(&self, shell: Shell) -> Script {
let text = match shell {
Shell::Bash => self.bash(),
Shell::Zsh => self.zsh(),
Shell::Fish => self.fish(),
};
Script { shell, text }
}
fn function(&self) -> String {
format!("__{}_first_prompt", self.bin.replace('-', "_"))
}
fn bash(&self) -> String {
let Self {
bin,
subcommand,
variable,
} = self;
let function = self.function();
formatdoc! {r#"
# Load in ~/.bashrc: eval "$({bin} init bash)"
{function}() {{
local status=$? declaration
if [[ -n ${{{function}_ran-}} ]]; then
return "${{status}}"
fi
{function}_ran=1
declaration=$(declare -p PROMPT_COMMAND 2>/dev/null)
if [[ ${{declaration}} == "declare -a"* ]]; then
local hooks=() hook
for hook in "${{PROMPT_COMMAND[@]}}"; do
[[ ${{hook}} == {function} ]] || hooks+=("${{hook}}")
done
PROMPT_COMMAND=("${{hooks[@]}}")
else
printf -v PROMPT_COMMAND %s "${{PROMPT_COMMAND//{function};/}}"
fi
if [[ -n ${{{variable}-}} ]] && type -P {bin} >/dev/null; then
command {bin} {subcommand}
fi
return "${{status}}"
}}
{function}_declaration=$(declare -p PROMPT_COMMAND 2>/dev/null)
if [[ ${{{function}_declaration}} == "declare -a"* ]]; then
if [[ " ${{PROMPT_COMMAND[*]}} " != *" {function} "* ]]; then
PROMPT_COMMAND=({function} "${{PROMPT_COMMAND[@]}}")
fi
elif [[ ${{PROMPT_COMMAND-}} != *{function}\;* ]]; then
printf -v PROMPT_COMMAND %s "{function};${{PROMPT_COMMAND-}}"
fi
unset {function}_declaration
"#}
}
fn zsh(&self) -> String {
let Self {
bin,
subcommand,
variable,
} = self;
let function = self.function();
formatdoc! {r#"
# Load in ~/.zshrc: eval "$({bin} init zsh)"
{function}() {{
precmd_functions=(${{precmd_functions:#{function}}})
if [[ -n ${{{variable}-}} ]] && whence -p {bin} >/dev/null; then
command {bin} {subcommand}
fi
}}
if (( ! ${{precmd_functions[(I){function}]:-0}} )); then
precmd_functions+=({function})
fi
"#}
}
fn fish(&self) -> String {
let Self {
bin,
subcommand,
variable,
} = self;
let function = self.function();
formatdoc! {r#"
# Load in ~/.config/fish/config.fish: {bin} init fish | source
function {function} --on-event fish_prompt
functions --erase {function}
if test -n "${variable}"; and command -q {bin}
command {bin} {subcommand}
end
end
"#}
}
}
fn is_command_word(word: &str) -> bool {
word.chars()
.next()
.is_some_and(|first| first.is_ascii_alphanumeric())
&& word
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
fn is_variable_name(name: &str) -> bool {
name.chars()
.next()
.is_some_and(|first| first.is_ascii_alphabetic() || first == '_')
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum HookError {
Bin(String),
Subcommand(String),
Variable(String),
}
impl fmt::Display for HookError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bin(bin) => write!(
f,
"binary name `{bin}` must be letters, digits, `-`, and `_`, starting with a letter or digit"
),
Self::Subcommand(subcommand) => write!(
f,
"subcommand `{subcommand}` must be letters, digits, `-`, and `_`, starting with a letter or digit"
),
Self::Variable(variable) => write!(
f,
"variable `{variable}` must be letters, digits, and `_`, not starting with a digit"
),
}
}
}
impl Error for HookError {}
#[derive(Debug, Clone, PartialEq, Eq, clap::Subcommand)]
pub enum InitCommand {
Init(InitArgs),
}
impl InitCommand {
#[must_use]
pub fn script(&self, hook: &Hook) -> Script {
match self {
Self::Init(args) => hook.script(args.shell),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, clap::Args)]
#[non_exhaustive]
pub struct InitArgs {
#[arg(value_enum)]
pub shell: Shell,
}