diff --git a/.mise-tasks/try-dotbee b/.mise-tasks/try-dotbee index 9710042..1bb1071 100755 --- a/.mise-tasks/try-dotbee +++ b/.mise-tasks/try-dotbee @@ -32,15 +32,6 @@ fi BIN_PATH="$(pwd)/target/${usage_target}/${BUILD_MODE}/dotbee" -echo -echo "Smoke-testing the binary inside the container..." -podman run --rm \ - $PODMAN_FLAGS \ - -v "${BIN_PATH}:/usr/local/bin/dotbee:Z" \ - dotbee-dev \ - bash -c "dotbee list && echo && dotbee doctor" -echo - echo "Running testing container..." podman run --hostname="laptop" -it --rm \ $PODMAN_FLAGS \ diff --git a/.nvim.lua b/.nvim.lua index bfac84a..9624ff3 100644 --- a/.nvim.lua +++ b/.nvim.lua @@ -1,6 +1,8 @@ vim.lsp.config("rust_analyzer", { ---@type lspconfig.settings.rust_analyzer settings = { - ["rust-analyzer"] = {}, + ["rust-analyzer"] = { + check = { command = "clippy" }, + }, }, }) diff --git a/Dockerfile b/Dockerfile index 6275874..e71ba56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,6 +50,13 @@ RUN < global/git/gitconfig email = test@localhost EOF +RUN << EOF cat > script.sh +#!/usr/bin/env bash +echo 'hello script' +EOF + +RUN chmod +x script.sh + # Create dotbee.toml RUN < dotbee.toml [settings] @@ -60,10 +67,18 @@ config = "~/.config" [profiles.global.links] gitconfig = { src = "global/git/gitconfig", dst = "~/.gitconfig" } +[profiles.laptop] +pre_hook = "echo 'hello laptop'" +post_hook.path = "./script.sh" + [profiles.laptop.links] nvim = { src = "profiles/laptop/nvim", dst = "{config}/nvim" } kitty = { src = "profiles/laptop/kitty", dst = "{config}/kitty" } +[profiles.termux] +pre_hook = "echo 'hello termux'" +post_hook = "goodbye 'termux'" + [profiles.termux.links] nvim = { src = "profiles/termux/nvim", dst = "{config}/nvim" } tmux = { src = "profiles/termux/tmux", dst = "{config}/tmux" } diff --git a/ROADMAP.md b/ROADMAP.md index 4544817..0920e86 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -45,7 +45,7 @@ config = "~/.config" nvim = { src = "editors/nvim/", dst = "{config}/nvim" } ``` -- [ ] **Pre/Post Hooks:** Run shell commands before/after profile switch (useful for things like enabling systemd services, but I wouldn't recommend for installing packages.). +- [x] **Pre/Post Hooks:** Run shell commands before/after profile switch (useful for things like enabling systemd services, but I wouldn't recommend for installing packages.). - [ ] **Dotfiles Fetching:** Dotbee should be able to fetch a remote dotfiles repository. - Using git - Using http (simply download it) diff --git a/docs/HOWTO.md b/docs/HOWTO.md index 137d3bb..f2ff2b4 100644 --- a/docs/HOWTO.md +++ b/docs/HOWTO.md @@ -115,3 +115,16 @@ nvim = { src = "editors/nvim/", dst = "{cfg}/nvim" } ``` Variables are plain strings and are not interpolated themselves. An undefined `{name}` causes a configuration error. + +### Pre/Post Hooks + +Hooks are simply scripts that get executed when switching between profiles. Each profile can have a `pre_hook` and a `post_hook`. + +- **pre_hook:** Runs prior to profile switch (useful for things like enabling systemd services for example). +- **post_hook:** Runs before switching into the new profile (useful for cleaning up things). + +```toml +[profiles.destktop] +pre_hook = "systemctl enable foo.service" +post_hook.path = "./path/to/script.sh" +``` diff --git a/src/context/config/hook.rs b/src/context/config/hook.rs new file mode 100644 index 0000000..a9e964d --- /dev/null +++ b/src/context/config/hook.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use nix::libc; +use serde::Deserialize; +use std::{path::PathBuf, process::Command}; + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum Hook { + File { path: PathBuf }, + Inline(String), +} + +impl Hook { + pub fn execute(&self) -> Result<()> { + match self { + Hook::File { path } => match Command::new(path).status() { + Ok(_status) => {} + Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => { + eprintln!("Error: The script at {:?} is not executable. Try 'chmod +x {:?}'", path, path); + } + Err(e) if e.raw_os_error() == Some(libc::ENOEXEC) => { + eprintln!( + "Error: Exec format error. Your script at '{:?}' is missing a shebang (e.g., #!/bin/sh) at the top of the file.", + path + ); + } + Err(e) => eprintln!("An unexpected error occurred: {}", e), + }, + Hook::Inline(code) => { + Command::new("sh").arg("-c").arg(code).status()?; + } + } + Ok(()) + } +} diff --git a/src/context/config/mod.rs b/src/context/config/mod.rs index d2678c3..edb55e1 100644 --- a/src/context/config/mod.rs +++ b/src/context/config/mod.rs @@ -1,4 +1,5 @@ pub mod conflict; +pub mod hook; pub mod vars; use anyhow::anyhow; @@ -8,6 +9,7 @@ use serde::Deserialize; use std::fs; use std::path::{Path, PathBuf}; +use crate::context::config::hook::Hook; use crate::utils::message; #[derive(Debug, Deserialize, Clone, Default)] @@ -28,6 +30,8 @@ pub struct Link { #[serde(deny_unknown_fields)] pub struct Profile { pub links: IndexMap, + pub pre_hook: Option, + pub post_hook: Option, } /// reserved profile name for links applied regardless of the active profile diff --git a/src/subcommands/purge.rs b/src/subcommands/purge.rs index fa7cc33..0e2ba0c 100644 --- a/src/subcommands/purge.rs +++ b/src/subcommands/purge.rs @@ -4,6 +4,14 @@ use colored::Colorize; use std::fs; pub fn run(context: &mut Context) -> anyhow::Result<(), anyhow::Error> { + // execute post_hook of the currently active profile + if let Some(stale_profile) = context.state.get_active_profile() + && let Some(ref hook) = context.config.get_profile(stale_profile)?.post_hook + { + println!("Running {} profile post hook", stale_profile); + hook.execute()?; + } + let links = context.state.get_links(); if links.is_empty() { diff --git a/src/subcommands/sync.rs b/src/subcommands/sync.rs index 90dbcc7..ab0962e 100644 --- a/src/subcommands/sync.rs +++ b/src/subcommands/sync.rs @@ -47,6 +47,20 @@ pub fn run(profile_name: Option, context: &mut crate::context::Context) let dotfiles_root = context.state.get_dotfiles_root()?; let dry_run = context.dry_run; + // execute post_hook of the currently active profile + if let Some(old_profile) = context.state.get_active_profile() + && let Some(ref hook) = context.config.get_profile(old_profile)?.post_hook + { + hook.execute()?; + } + + // execute pre_hook of the new profile + if let Some(new_profile) = &target_profile + && let Some(ref hook) = context.config.get_profile(new_profile)?.pre_hook + { + hook.execute()? + } + // build desired links map from config let mut desired_links: IndexMap = IndexMap::new();