zenops 0.20.0

Declarative system configuration management for shell config and dotfiles.
use std::sync::Arc;

use zenops_safe_relative_path::{SafeRelativePathBuf, SinglePathComponent};

use crate::config_files::{ConfigFilePath, ConfigFileSource};

use super::{Config, ConfigFiles, Error, stored_relative_path::StoredRelativePath};

/// Where the dotfiles for a package live inside your zenops repo, and
/// which of them should be symlinked into your home tree.
///
/// `source` is the directory inside the repo (e.g. `configs/helix`).
/// `symlinks` lists the files within that directory to surface — every
/// listed file becomes a symlink at the destination, with the same
/// relative path. Anything in `source` that isn't listed in `symlinks`
/// is ignored.
#[derive(serde::Deserialize, schemars::JsonSchema, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields)]
pub(super) struct StoredConfigFiles {
    /// Directory in the zenops repo that holds this package's
    /// dotfiles. Relative to the repo root.
    source: StoredRelativePath,
    /// Files inside `source` to symlink. Paths are relative to
    /// `source`; the same relative path is used at the destination.
    #[serde(default)]
    symlinks: Vec<StoredRelativePath>,
}

impl StoredConfigFiles {
    fn update_config_files<'a>(
        &'a self,
        _config: &Config,
        config_files: &mut ConfigFiles,
        make_config_path: impl Fn(&'a StoredRelativePath) -> ConfigFilePath,
    ) -> Result<(), Error> {
        for symlink in &self.symlinks {
            config_files.add(
                make_config_path(symlink),
                ConfigFileSource::SymlinkFrom(ConfigFilePath::Zenops(Arc::from(
                    self.source.safe_join(symlink),
                ))),
            );
        }
        Ok(())
    }
}

/// A bundle of dotfiles owned by a package — where they live in your
/// zenops repo and where they should land in your home.
///
/// Pick `type = ".config"` for files that belong under
/// `~/.config/<dir>/` (the modern convention), or `type = "home"` for
/// files that live directly under `~/`. zenops symlinks each file from
/// the zenops repo to its destination, so editing either side updates
/// the other.
#[derive(serde::Deserialize, schemars::JsonSchema, Debug, Clone, PartialEq)]
#[serde(deny_unknown_fields, tag = "type")]
pub(super) enum PkgConfigFiles {
    /// Files that belong under `~/.config/<dir>/`.
    #[serde(rename = ".config")]
    DotConfig {
        /// Which subdirectory of `~/.config/` the files belong in.
        /// Defaults to the package key, so `[pkg.helix]` lands in
        /// `~/.config/helix/` automatically. Set this only when the
        /// directory name diverges from the package key — for
        /// example, a package keyed `neovim` whose config dir is
        /// `nvim`.
        #[serde(default)]
        name: Option<SinglePathComponent>,
        #[serde(flatten)]
        configs: StoredConfigFiles,
    },
    /// Files that live directly under `~/`.
    #[serde(rename = "home")]
    Home {
        /// Subdirectory of `~` the files belong in. Use an empty
        /// string for files that sit at the very top of `~` (like
        /// `.bashrc`).
        dir: SafeRelativePathBuf,
        #[serde(flatten)]
        configs: StoredConfigFiles,
    },
}

impl PkgConfigFiles {
    pub fn update_config_files(
        &self,
        pkg_key: &str,
        config: &Config,
        config_files: &mut ConfigFiles,
    ) -> Result<(), Error> {
        match self {
            Self::DotConfig { name, configs } => {
                let fallback;
                let dir: &SinglePathComponent = match name {
                    Some(n) => n,
                    None => {
                        fallback = SinglePathComponent::try_new(pkg_key)?;
                        &fallback
                    }
                };
                configs.update_config_files(config, config_files, |symlink| {
                    ConfigFilePath::DotConfig(Arc::from(dir.safe_join(symlink)))
                })?
            }
            Self::Home { dir, configs } => {
                configs.update_config_files(config, config_files, |symlink| {
                    ConfigFilePath::Home(Arc::from(dir.safe_join(symlink)))
                })?
            }
        }
        Ok(())
    }
}