Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: shell hooks #3414

Merged
merged 1 commit into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,30 @@ Hooks are executed with the following environment variables set:
- `MISE_ORIGINAL_CWD`: The directory that the user is in.
- `MISE_PROJECT_DIR`: The root directory of the project.
- `MISE_PREVIOUS_DIR`: The directory that the user was in before the directory change (only if a directory change occurred).

## Shell hooks

Hooks can be executed in the current shell, for example if you'd like to add bash completions when entering a directory:

```toml
[hooks.enter]
shell = "bash"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the docs tell you somewhere else what the official mise names of the shells are? eg I might not guess that I have to capitalize Zsh (if I'm reading the code properly).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh that's a bug

script = "source completions.sh"
```

## Multiple hooks syntax

You can use arrays to define multiple hooks in the same file:

```toml
[hooks]
enter = [
"echo 'I entered the project'",
"echo 'I am in the project'"
]

[[hooks.cd]]
script = "echo 'I changed directories'"
[[hooks.cd]]
script = "echo 'I also directories'"
```
30 changes: 28 additions & 2 deletions schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -961,8 +961,34 @@
"description": "hooks to run",
"type": "object",
"additionalProperties": {
"description": "script to run",
"type": "string"
"oneOf": [
{
"description": "script to run",
"type": "string"
},
{
"description": "script to run",
"items": {
"description": "script to run",
"type": "string"
},
"type": "array"
},
{
"additionalProperties": false,
"properties": {
"script": {
"description": "script to run",
"type": "string"
},
"shell": {
"description": "specify the shell to run the script inside of",
"type": "string"
}
},
"type": "object"
}
]
}
},
"watch_files": {
Expand Down
42 changes: 37 additions & 5 deletions src/cli/hook_env.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
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 std::env::{join_paths, split_paths};
use std::ops::Deref;
use std::path::PathBuf;

use crate::config::{Config, Settings};
use crate::direnv::DirenvDiff;
use crate::env::{PATH_KEY, TERM_WIDTH, __MISE_DIFF};
use crate::env_diff::{EnvDiff, EnvDiffOperation};
use crate::hook_env::WatchFilePattern;
use crate::shell::{get_shell, ShellType};
use crate::hooks::Hooks;
use crate::shell::{get_shell, Shell, ShellType};
use crate::toolset::{Toolset, ToolsetBuilder};
use crate::{dirs, env, hook_env, hooks, watch_files};

Expand Down Expand Up @@ -66,12 +66,44 @@ impl HookEnv {
let output = hook_env::build_env_commands(&*shell, &patches);
miseprint!("{output}")?;
self.display_status(&config, &ts)?;

self.run_shell_hooks(&config, &*shell)?;
hooks::run_all_hooks(&ts);
watch_files::execute_runs(&ts);

Ok(())
}

fn run_shell_hooks(&self, config: &Config, shell: &dyn Shell) -> Result<()> {
let hooks = config.hooks()?;
for h in hooks::SCHEDULED_HOOKS.lock().unwrap().iter() {
let hooks = hooks
.iter()
.map(|(_p, hook)| hook)
.filter(|hook| hook.hook == *h && hook.shell == Some(shell.to_string()))
.collect_vec();
match *h {
Hooks::Enter => {
for hook in hooks {
miseprintln!("{}", hook.script);
}
}
Hooks::Cd => {
for hook in hooks {
miseprintln!("{}", hook.script);
}
}
Hooks::Leave => {
for _hook in hooks {
warn!("leave hook not yet implemented");
}
}
_ => {}
}
}
Ok(())
}

fn display_status(&self, config: &Config, ts: &Toolset) -> Result<()> {
let settings = Settings::get();
if self.status || settings.status.show_tools {
Expand Down
19 changes: 13 additions & 6 deletions src/config/config_file/mise_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub struct MiseToml {
#[serde(skip)]
doc: OnceCell<DocumentMut>,
#[serde(default)]
hooks: IndexMap<Hooks, String>,
hooks: IndexMap<Hooks, toml::Value>,
#[serde(default)]
tools: IndexMap<BackendArg, MiseTomlToolList>,
#[serde(default)]
Expand Down Expand Up @@ -455,13 +455,20 @@ impl ConfigFile for MiseToml {
}

fn hooks(&self) -> eyre::Result<Vec<Hook>> {
self.hooks
Ok(self
.hooks
.iter()
.map(|(hook, run)| {
let run = self.parse_template(run)?;
Ok(Hook { hook: *hook, run })
.map(|(hook, val)| {
let mut hooks = Hook::from_toml(*hook, val.clone())?;
for hook in hooks.iter_mut() {
hook.script = self.parse_template(&hook.script)?;
}
eyre::Ok(hooks)
})
.collect()
.collect::<eyre::Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect())
}

fn vars(&self) -> eyre::Result<&IndexMap<String, String>> {
Expand Down
46 changes: 42 additions & 4 deletions src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::cmd::cmd;
use crate::config::{Config, SETTINGS};
use crate::toolset::Toolset;
use crate::{dirs, hook_env};
use eyre::Result;
use eyre::{eyre, Result};
use indexmap::IndexSet;
use itertools::Itertools;
use once_cell::sync::Lazy;
Expand Down Expand Up @@ -35,7 +35,8 @@ pub enum Hooks {
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Hook {
pub hook: Hooks,
pub run: String,
pub script: String,
pub shell: Option<String>,
}

pub static SCHEDULED_HOOKS: Lazy<Mutex<IndexSet<Hooks>>> = Lazy::new(Default::default);
Expand All @@ -56,7 +57,7 @@ pub fn run_one_hook(ts: &Toolset, hook: Hooks) {
let config = Config::get();
let hooks = config.hooks().unwrap_or_default();
for (root, h) in hooks {
if hook != h.hook {
if hook != h.hook || h.shell.is_some() {
continue;
}
trace!("running hook {hook} in {root:?}");
Expand Down Expand Up @@ -86,6 +87,43 @@ pub fn run_one_hook(ts: &Toolset, hook: Hooks) {
}
}

impl Hook {
pub fn from_toml(hook: Hooks, value: toml::Value) -> Result<Vec<Self>> {
match value {
toml::Value::String(run) => Ok(vec![Hook {
hook,
script: run,
shell: None,
}]),
toml::Value::Table(tbl) => {
let script = tbl
.get("script")
.ok_or_else(|| eyre!("missing `script` key"))?;
let script = script
.as_str()
.ok_or_else(|| eyre!("`run` must be a string"))?;
let shell = tbl
.get("shell")
.and_then(|s| s.as_str())
.map(|s| s.to_string());
Ok(vec![Hook {
hook,
script: script.to_string(),
shell,
}])
}
toml::Value::Array(arr) => {
let mut hooks = vec![];
for v in arr {
hooks.extend(Self::from_toml(hook, v)?);
}
Ok(hooks)
}
v => panic!("invalid hook value: {v}"),
}
}
}

fn execute(ts: &Toolset, root: &Path, hook: &Hook) -> Result<()> {
SETTINGS.ensure_experimental("hooks")?;
#[cfg(unix)]
Expand All @@ -97,7 +135,7 @@ fn execute(ts: &Toolset, root: &Path, hook: &Hook) -> Result<()> {
.iter()
.skip(1)
.map(|s| s.as_str())
.chain(once(hook.run.as_str()))
.chain(once(hook.script.as_str()))
.collect_vec();
let mut env = ts.full_env()?;
if let Some(cwd) = dirs::CWD.as_ref() {
Expand Down
7 changes: 7 additions & 0 deletions src/shell/bash.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt::Display;
use std::path::Path;

use indoc::formatdoc;
Expand Down Expand Up @@ -104,6 +105,12 @@ impl Shell for Bash {
}
}

impl Display for Bash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "bash")
}
}

#[cfg(test)]
mod tests {
use insta::assert_snapshot;
Expand Down
10 changes: 8 additions & 2 deletions src/shell/elvish.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::fmt::Display;
use std::path::Path;

use indoc::formatdoc;

use crate::shell::Shell;
use indoc::formatdoc;

#[derive(Default)]
pub struct Elvish {}
Expand Down Expand Up @@ -84,6 +84,12 @@ impl Shell for Elvish {
}
}

impl Display for Elvish {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "elvish")
}
}

#[cfg(test)]
mod tests {
use insta::assert_snapshot;
Expand Down
7 changes: 7 additions & 0 deletions src/shell/fish.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt::{Display, Formatter};
use std::path::Path;

use crate::config::Settings;
Expand Down Expand Up @@ -126,6 +127,12 @@ impl Shell for Fish {
}
}

impl Display for Fish {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "fish")
}
}

#[cfg(test)]
mod tests {
use insta::assert_snapshot;
Expand Down
2 changes: 1 addition & 1 deletion src/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl Display for ShellType {
}
}

pub trait Shell {
pub trait Shell: Display {
fn activate(&self, exe: &Path, flags: String) -> String;
fn deactivate(&self) -> String;
fn set_env(&self, k: &str, v: &str) -> String;
Expand Down
6 changes: 6 additions & 0 deletions src/shell/nushell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ impl Shell for Nushell {
}
}

impl Display for Nushell {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "nu")
}
}

#[cfg(test)]
mod tests {
use insta::assert_snapshot;
Expand Down
7 changes: 7 additions & 0 deletions src/shell/xonsh.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::fmt::Display;
use std::path::Path;

use indoc::formatdoc;
Expand Down Expand Up @@ -138,6 +139,12 @@ impl Shell for Xonsh {
}
}

impl Display for Xonsh {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "xonsh")
}
}

#[cfg(test)]
mod tests {
use insta::assert_snapshot;
Expand Down
7 changes: 7 additions & 0 deletions src/shell/zsh.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt::Display;
use std::path::Path;

use indoc::formatdoc;
Expand Down Expand Up @@ -104,6 +105,12 @@ impl Shell for Zsh {
}
}

impl Display for Zsh {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Zsh")
}
}

#[cfg(test)]
mod tests {
use insta::assert_snapshot;
Expand Down
Loading