Skip to content

Commit

Permalink
feat: shell hooks (#3414)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx authored Dec 8, 2024
1 parent 528f29f commit d6c2cd9
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 20 deletions.
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"
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

0 comments on commit d6c2cd9

Please sign in to comment.