use crate::config::{Config, SETTINGS};
use crate::direnv::DirenvDiff;
use crate::env::{join_paths, split_paths, PATH_ENV_SEP};
use crate::env::{PATH_KEY, TERM_WIDTH, __MISE_DIFF};
use crate::env_diff::{EnvDiff, EnvDiffOperation, EnvMap};
use crate::file::display_rel_path;
use crate::hook_env::{WatchFilePattern, PREV_SESSION};
use crate::shell::{get_shell, ShellType};
use crate::toolset::Toolset;
use crate::{env, hook_env, hooks, watch_files};
use console::truncate_str;
use eyre::Result;
use indexmap::IndexSet;
use itertools::Itertools;
use std::collections::BTreeSet;
use std::ops::Deref;
use std::path::PathBuf;
#[derive(Debug, clap::Args)]
#[clap(hide = true)]
pub struct HookEnv {
#[clap(long, short)]
shell: Option<ShellType>,
#[clap(long, short)]
force: bool,
#[clap(long, hide = true)]
status: bool,
#[clap(long, short)]
quiet: bool,
}
impl HookEnv {
pub fn run(self) -> Result<()> {
let config = Config::try_get()?;
let watch_files = config.watch_files()?;
time!("hook-env");
if !self.force && hook_env::should_exit_early(watch_files.clone()) {
trace!("should_exit_early true");
return Ok(());
}
time!("should_exit_early false");
let ts = config.get_toolset()?;
let shell = get_shell(self.shell).expect("no shell provided, use `--shell=zsh`");
miseprint!("{}", hook_env::clear_old_env(&*shell))?;
let (mut mise_env, env_results) = ts.final_env(&config)?;
mise_env.remove(&*PATH_KEY);
self.display_status(&config, ts, &mise_env)?;
let mut diff = EnvDiff::new(&env::PRISTINE_ENV, mise_env.clone());
let mut patches = diff.to_patches();
let paths = ts.list_final_paths(&config, env_results)?;
diff.path.clone_from(&paths);
patches.extend(self.build_path_operations(&paths, &__MISE_DIFF.path)?);
patches.push(self.build_diff_operation(&diff)?);
patches.push(self.build_session_operation(ts, mise_env, watch_files)?);
let output = hook_env::build_env_commands(&*shell, &patches);
miseprint!("{output}")?;
hooks::run_all_hooks(ts, &*shell);
watch_files::execute_runs(ts);
Ok(())
}
fn display_status(&self, config: &Config, ts: &Toolset, cur_env: &EnvMap) -> Result<()> {
if self.status || SETTINGS.status.show_tools {
let prev = &PREV_SESSION.loaded_tools;
let cur = ts
.list_current_installed_versions()
.into_iter()
.rev()
.map(|(_, tv)| format!("{}@{}", tv.short(), tv.version))
.collect::<IndexSet<_>>();
let removed = prev.difference(&cur).collect::<IndexSet<_>>();
let new = cur.difference(prev).collect::<IndexSet<_>>();
if !new.is_empty() {
let status = new.into_iter().map(|t| format!("+{t}")).rev().join(" ");
info!("{}", truncate_str(&status, TERM_WIDTH.max(60) - 5, "…"));
}
if !removed.is_empty() {
let status = removed.into_iter().map(|t| format!("-{t}")).rev().join(" ");
info!("{}", truncate_str(&status, TERM_WIDTH.max(60) - 5, "…"));
}
}
if self.status || SETTINGS.status.show_env {
let mut env_diff = EnvDiff::new(&PREV_SESSION.env, cur_env.clone()).to_patches();
let removed_keys = PREV_SESSION
.env
.keys()
.collect::<IndexSet<_>>()
.difference(&cur_env.keys().collect::<IndexSet<_>>())
.map(|k| EnvDiffOperation::Remove(k.to_string()))
.collect_vec();
env_diff.extend(removed_keys);
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, "…"));
}
let new_paths: IndexSet<PathBuf> = config
.path_dirs()
.map(|p| p.iter().cloned().collect())
.unwrap_or_default();
let old_paths = &PREV_SESSION.config_paths;
let removed_paths = old_paths.difference(&new_paths).collect::<IndexSet<_>>();
let added_paths = new_paths.difference(old_paths).collect::<IndexSet<_>>();
if !added_paths.is_empty() {
let status = added_paths
.iter()
.map(|p| format!("+{}", display_rel_path(p)))
.join(" ");
info!("{}", truncate_str(&status, TERM_WIDTH.max(60) - 5, "…"));
}
if !removed_paths.is_empty() {
let status = removed_paths
.iter()
.map(|p| format!("-{}", display_rel_path(p)))
.join(" ");
info!("{}", truncate_str(&status, TERM_WIDTH.max(60) - 5, "…"));
}
}
ts.notify_if_versions_missing();
Ok(())
}
fn build_path_operations(
&self,
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!("{PATH_ENV_SEP}{orig_path}")) {
Some((pre, post)) if !SETTINGS.activate_aggressive => (
split_paths(pre).collect_vec(),
split_paths(&format!("{orig_path}{post}")).collect_vec(),
),
_ => (vec![], split_paths(&full).collect_vec()),
},
None => (vec![], split_paths(&full).collect_vec()),
};
let new_path = join_paths(pre.iter().chain(installs.iter()).chain(post.iter()))?
.to_string_lossy()
.into_owned();
let mut ops = vec![EnvDiffOperation::Add(PATH_KEY.to_string(), 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_session_operation(
&self,
ts: &Toolset,
env: EnvMap,
watch_files: BTreeSet<WatchFilePattern>,
) -> Result<EnvDiffOperation> {
let loaded_tools = if self.status || SETTINGS.status.show_tools {
ts.list_current_versions()
.into_iter()
.map(|(_, tv)| format!("{}@{}", tv.short(), tv.version))
.collect()
} else {
Default::default()
};
let session = hook_env::build_session(env, loaded_tools, watch_files)?;
Ok(EnvDiffOperation::Add(
"__MISE_SESSION".into(),
hook_env::serialize(&session)?,
))
}
}
fn patch_to_status(patch: EnvDiffOperation) -> String {
match patch {
EnvDiffOperation::Add(k, _) => format!("+{}", k),
EnvDiffOperation::Change(k, _) => format!("~{}", k),
EnvDiffOperation::Remove(k) => format!("-{}", k),
}
}