pub(crate) mod pkg;
mod shell;
mod stored_config_files;
mod stored_relative_path;
use std::{path::Path, sync::Arc};
use indexmap::IndexMap;
use smol_str::SmolStr;
use xshell::cmd;
use zenops_safe_relative_path::srpath;
pub use crate::config::pkg::PkgConfig;
use crate::{
config::{
pkg::{Shell, ShellInitAction, StoredPkgConfig},
shell::StoredShellEnvironment,
stored_config_files::StoredConfigFilesBase,
},
config_files::{ConfigFileDirs, ConfigFilePath, ConfigFiles},
error::Error,
git::Git,
output::{Output, ResolvedConfigFilePath, Status},
};
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(deny_unknown_fields, default)]
struct StoredConfig {
shell: StoredShellEnvironment,
configs: Vec<StoredConfigFilesBase>,
pkg: IndexMap<SmolStr, StoredPkgConfig>,
}
pub struct Config<'dirs> {
dirs: &'dirs ConfigFileDirs,
zenops_repo: ResolvedConfigFilePath,
stored: StoredConfig,
pkgs: IndexMap<SmolStr, PkgConfig>,
}
fn deep_merge(base: &mut toml::Value, overlay: toml::Value) {
match (base, overlay) {
(toml::Value::Table(b), toml::Value::Table(o)) => {
for (k, v) in o {
deep_merge(
b.entry(k).or_insert(toml::Value::Table(Default::default())),
v,
);
}
}
(base, overlay) => *base = overlay,
}
}
impl<'dirs> Config<'dirs> {
pub fn load(
dirs: &'dirs ConfigFileDirs,
sh: &xshell::Shell,
update_self: bool,
) -> Result<Self, Error> {
if update_self {
let zenops_dir = dirs.zenops();
cmd!(sh, "git -C {zenops_dir} pull --rebase").run()?;
}
let zenops_repo =
ResolvedConfigFilePath::resolve(ConfigFilePath::Zenops(Arc::from(srpath!(""))), dirs);
let path = dirs.zenops().join("config.toml");
let defaults_str = include_str!("config/defaults.toml");
let mut merged: toml::Value = toml::from_str(defaults_str)
.map_err(|e| Error::ParseDb(std::path::PathBuf::from("<defaults>"), e))?;
let user_bytes = std::fs::read(&path).map_err(|e| Error::OpenDb(path.clone(), e))?;
let user_val: toml::Value =
toml::from_slice(&user_bytes).map_err(|e| Error::ParseDb(path.to_path_buf(), e))?;
deep_merge(&mut merged, user_val);
let stored: StoredConfig = merged
.try_into()
.map_err(|e| Error::ParseDb(path.to_path_buf(), e))?;
let mut pkgs = IndexMap::new();
for (k, v) in stored.pkg.iter() {
pkgs.insert(k.clone(), v.clone().resolve(k)?);
}
Ok(Self {
dirs,
zenops_repo,
stored,
pkgs,
})
}
#[cfg(target_os = "macos")]
pub fn has_brew_llvm(&self) -> bool {
Path::new("/opt/homebrew/opt/llvm").exists()
}
#[cfg(target_os = "macos")]
pub fn has_brew_python(&self) -> bool {
Path::new("/opt/homebrew/opt/python").exists()
}
pub fn pkgs(&self) -> &IndexMap<SmolStr, PkgConfig> {
&self.pkgs
}
pub fn home(&self) -> &Path {
self.dirs.home()
}
pub(crate) fn env_pkg_inits(&self, shell: Shell) -> Vec<&ShellInitAction> {
self.pkgs
.values()
.filter(|p| p.is_installed(self.dirs.home()))
.flat_map(|p| p.env_init.for_shell(shell))
.collect()
}
pub(crate) fn interactive_pkg_inits(&self, shell: Shell) -> Vec<&ShellInitAction> {
self.pkgs
.values()
.filter(|p| p.is_installed(self.dirs.home()))
.flat_map(|p| p.interactive_init.for_shell(shell))
.collect()
}
pub fn path_variable(&self) -> Option<String> {
let mut paths = "$PATH".to_string();
#[cfg(target_os = "macos")]
if self.has_brew_python() {
paths.push_str(":$(brew --prefix python)/libexec/bin");
}
#[cfg(target_os = "macos")]
if self.has_brew_llvm() {
paths.insert_str(0, "$(brew --prefix)/opt/llvm/bin:");
}
paths.push_str(":~/.local/bin");
Some(paths)
}
pub fn update_config_files(
&self,
_sh: &xshell::Shell,
config_files: &mut ConfigFiles<'_>,
) -> Result<(), Error> {
self.stored.shell.update_config_files(self, config_files)?;
for config in &self.stored.configs {
config.update_config_files(self, config_files)?;
}
Ok(())
}
pub fn check_own_status(
&self,
sh: &xshell::Shell,
output: &mut dyn Output,
) -> Result<(), Error> {
let git = Git::new(self.dirs.zenops(), sh);
if git.is_git_repo()? {
for status in git.status()? {
output.push_status(Status::Git {
repo: self.zenops_repo.clone(),
status,
});
}
}
Ok(())
}
}