use std::env::{join_paths, split_paths};
use std::ops::Deref;
use std::path::PathBuf;
use console::truncate_str;
use eyre::Result;
use itertools::Itertools;
use crate::config::Config;
use crate::config::Settings;
use crate::direnv::DirenvDiff;
use crate::env::{TERM_WIDTH, __MISE_DIFF};
use crate::env_diff::{EnvDiff, EnvDiffOperation};
use crate::shell::{get_shell, ShellType};
use crate::toolset::{Toolset, ToolsetBuilder};
use crate::{env, hook_env};
#[derive(Debug, clap::Args)]
#[clap(hide = true)]
pub struct HookEnv {
#[clap(long, short)]
shell: Option<ShellType>,
#[clap(long)]
status: bool,
#[clap(long, short)]
quiet: bool,
}
impl HookEnv {
pub fn run(self) -> Result<()> {
let config = Config::try_get()?;
let watch_files: Vec<_> = config
.config_files
.values()
.flat_map(|p| p.watch_files())
.collect();
if hook_env::should_exit_early(&watch_files) {
return Ok(());
}
let ts = ToolsetBuilder::new().build(&config)?;
let shell = get_shell(self.shell).expect("no shell provided, use `--shell=zsh`");
miseprint!("{}", hook_env::clear_old_env(&*shell));
let mut env = ts.env(&config);
let env_path = env.remove("PATH");
let mut diff = EnvDiff::new(&env::PRISTINE_ENV, env);
let mut patches = diff.to_patches();
let mut paths = config.path_dirs.clone();
if let Some(p) = env_path {
paths.extend(split_paths(&p).collect_vec());
}
paths.extend(ts.list_paths()); diff.path = paths.clone();
let settings = Settings::try_get()?;
patches.extend(self.build_path_operations(&settings, &paths, &__MISE_DIFF.path)?);
patches.push(self.build_diff_operation(&diff)?);
patches.push(self.build_watch_operation(&watch_files)?);
let output = hook_env::build_env_commands(&*shell, &patches);
miseprint!("{output}");
if self.status {
self.display_status(&config, &ts);
ts.notify_if_versions_missing();
}
Ok(())
}
fn display_status(&self, config: &Config, ts: &Toolset) {
let installed_versions = ts
.list_current_installed_versions()
.into_iter()
.rev()
.map(|(_, v)| v.to_string())
.collect_vec();
if !installed_versions.is_empty() {
let status = installed_versions.into_iter().rev().join(" ");
info!("{}", truncate_str(&status, TERM_WIDTH.max(60) - 5, "…"));
}
let env_diff = EnvDiff::new(&env::PRISTINE_ENV, config.env.clone()).to_patches();
if !env_diff.is_empty() {
let env_diff = env_diff.into_iter().map(patch_to_status).join(" ");
info!("{}", truncate_str(&env_diff, TERM_WIDTH.max(60) - 5, "…"));
}
}
fn build_path_operations(
&self,
settings: &Settings,
installs: &Vec<PathBuf>,
to_remove: &Vec<PathBuf>,
) -> Result<Vec<EnvDiffOperation>> {
let full = join_paths(&*env::PATH)?.to_string_lossy().to_string();
let (pre, post) = match &*env::__MISE_ORIG_PATH {
Some(orig_path) => match full.split_once(&format!(":{orig_path}")) {
Some((pre, post)) if !settings.activate_aggressive => {
(pre.to_string(), (orig_path.to_string() + post))
}
_ => (String::new(), full),
},
None => (String::new(), full),
};
let install_path = join_paths(installs)?.to_string_lossy().to_string();
let new_path = vec![pre, install_path, post]
.into_iter()
.filter(|p| !p.is_empty())
.join(":");
let mut ops = vec![EnvDiffOperation::Add("PATH".into(), new_path)];
if let Some(input) = env::DIRENV_DIFF.deref() {
match self.update_direnv_diff(input, installs, to_remove) {
Ok(Some(op)) => {
ops.push(op);
}
Err(err) => warn!("failed to update DIRENV_DIFF: {:#}", err),
_ => {}
}
}
Ok(ops)
}
fn update_direnv_diff(
&self,
input: &str,
installs: &Vec<PathBuf>,
to_remove: &Vec<PathBuf>,
) -> Result<Option<EnvDiffOperation>> {
let mut diff = DirenvDiff::parse(input)?;
if diff.new_path().is_empty() {
return Ok(None);
}
for path in to_remove {
diff.remove_path_from_old_and_new(path)?;
}
for install in installs {
diff.add_path_to_old_and_new(install)?;
}
Ok(Some(EnvDiffOperation::Change(
"DIRENV_DIFF".into(),
diff.dump()?,
)))
}
fn build_diff_operation(&self, diff: &EnvDiff) -> Result<EnvDiffOperation> {
Ok(EnvDiffOperation::Add(
"__MISE_DIFF".into(),
diff.serialize()?,
))
}
fn build_watch_operation(&self, watch_files: &[PathBuf]) -> Result<EnvDiffOperation> {
let watches = hook_env::build_watches(watch_files)?;
Ok(EnvDiffOperation::Add(
"__MISE_WATCH".into(),
hook_env::serialize_watches(&watches)?,
))
}
}
fn patch_to_status(patch: EnvDiffOperation) -> String {
match patch {
EnvDiffOperation::Add(k, _) => format!("+{}", k),
EnvDiffOperation::Change(k, _) => format!("~{}", k),
EnvDiffOperation::Remove(k) => format!("-{}", k),
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_hook_env() {
assert_cli!("hook-env", "--status", "-s", "fish");
}
}