use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use rich_ext::cli_doc::{Explanation, Layer as PrecedenceLayer, Precedence};
use crate::error::MicroError;
use crate::model::{Layer, MicroAsset, Origin};
use crate::name::check_name;
use crate::package::{self, Limits, Loaded};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MicroRoots {
pub builtin_set: bool,
pub builtin: Option<PathBuf>,
pub user: Option<PathBuf>,
pub project: Option<PathBuf>,
pub project_trusted: bool,
}
impl MicroRoots {
pub fn from_env(project_root: Option<&Path>) -> Self {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from);
MicroRoots {
builtin_set: true,
builtin: None,
user: home.as_deref().map(Self::user_dir),
project: project_root.map(Self::project_dir),
project_trusted: false,
}
}
pub fn user_dir(home: &Path) -> PathBuf {
home.join(".config").join("rich").join("micro")
}
pub fn project_dir(project_root: &Path) -> PathBuf {
project_root.join(".rich").join("micro")
}
#[must_use]
pub fn trust_project(mut self, trusted: bool) -> Self {
self.project_trusted = trusted;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Rejected {
pub layer: Layer,
pub path: PathBuf,
pub package: Option<String>,
pub error: MicroError,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Collision {
pub layer: Layer,
pub name: String,
pub kept: String,
pub dropped: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LoadReport {
pub rejected: Vec<Rejected>,
pub collisions: Vec<Collision>,
pub untrusted_project: Option<PathBuf>,
}
#[derive(Clone, Debug, Default)]
struct LayerAssets {
origin: Option<String>,
assets: BTreeMap<String, Arc<MicroAsset>>,
aliases: BTreeMap<String, String>,
}
#[derive(Clone, Debug, Default)]
pub struct MicroRegistry {
layers: [LayerAssets; 4],
}
impl MicroRegistry {
pub fn new() -> Self {
let mut registry = MicroRegistry::default();
registry.layers[Layer::BuiltIn.index()].origin = Some("rs-rich-micro".to_string());
registry.layers[Layer::Inline.index()].origin = Some("code".to_string());
registry
}
pub fn builtin() -> Self {
let mut registry = MicroRegistry::new();
registry.load_builtin_set(&mut LoadReport::default());
registry
}
pub fn load_builtin_set(&mut self, report: &mut LoadReport) {
for (pack, loaded) in crate::builtin::PACKS
.iter()
.zip(crate::builtin::packs(Layer::BuiltIn))
{
let path = PathBuf::from(format!("builtin:{pack}"));
match loaded {
Ok(pack) => {
for asset in pack.assets {
report.collisions.extend(self.insert(Layer::BuiltIn, asset));
}
for (package, error) in pack.rejected {
report.rejected.push(Rejected {
layer: Layer::BuiltIn,
path: path.clone(),
package: Some(package),
error,
});
}
}
Err(error) => report.rejected.push(Rejected {
layer: Layer::BuiltIn,
path,
package: None,
error,
}),
}
}
}
pub fn load(roots: &MicroRoots, limits: &Limits) -> (Self, LoadReport) {
let mut registry = MicroRegistry::new();
let mut report = LoadReport::default();
if roots.builtin_set {
registry.load_builtin_set(&mut report);
}
if let Some(dir) = &roots.builtin {
registry.load_dir(Layer::BuiltIn, dir, limits, &mut report);
}
if let Some(dir) = &roots.user {
registry.load_dir(Layer::User, dir, limits, &mut report);
}
if let Some(dir) = &roots.project {
if roots.project_trusted {
registry.load_dir(Layer::Project, dir, limits, &mut report);
} else if dir.is_dir() {
report.untrusted_project = Some(dir.clone());
}
}
(registry, report)
}
pub fn load_dir(&mut self, layer: Layer, dir: &Path, limits: &Limits, report: &mut LoadReport) {
if layer != Layer::BuiltIn || self.layers[layer.index()].assets.is_empty() {
self.layers[layer.index()].origin = Some(dir.display().to_string());
}
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
Err(error) => {
report.rejected.push(Rejected {
layer,
path: dir.to_path_buf(),
package: None,
error: MicroError::Io(format!("{}: {error}", dir.display())),
});
return;
}
};
let mut paths: Vec<PathBuf> = entries
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if name.starts_with('.') || name.is_empty() {
return false;
}
path.is_dir()
|| path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e == "richmicro" || e == "zip")
})
.collect();
paths.sort();
for path in paths {
match package::load(&path, layer, limits) {
Ok(Loaded::Package(asset)) => {
report.collisions.extend(self.insert(layer, *asset));
}
Ok(Loaded::Pack(pack)) => {
for asset in pack.assets {
report.collisions.extend(self.insert(layer, asset));
}
for (package, error) in pack.rejected {
report.rejected.push(Rejected {
layer,
path: path.clone(),
package: Some(package),
error,
});
}
}
Err(error) => report.rejected.push(Rejected {
layer,
path,
package: None,
error,
}),
}
}
}
pub fn add(&mut self, layer: Layer, asset: MicroAsset) -> Result<Vec<Collision>, MicroError> {
asset.validate()?;
Ok(self.insert(layer, asset))
}
fn insert(&mut self, layer: Layer, asset: MicroAsset) -> Vec<Collision> {
let origin = Origin {
layer,
..asset.origin().clone()
};
let asset = asset.with_origin(origin);
let slot = &mut self.layers[layer.index()];
let mut collisions = Vec::new();
let claim = |name: &str, asset: &MicroAsset| format!("{name} from {}", asset.origin());
let name = asset.name().to_string();
let existing = slot.assets.get(&name).cloned().or_else(|| {
slot.aliases
.get(&name)
.and_then(|target| slot.assets.get(target).cloned())
});
if let Some(existing) = existing {
collisions.push(Collision {
layer,
name: name.clone(),
kept: claim(existing.name(), &existing),
dropped: claim(&name, &asset),
});
return collisions;
}
for alias in asset.aliases() {
let taken = slot
.assets
.get(alias)
.or_else(|| slot.aliases.get(alias).and_then(|t| slot.assets.get(t)))
.cloned();
match taken {
Some(existing) => collisions.push(Collision {
layer,
name: alias.clone(),
kept: claim(existing.name(), &existing),
dropped: format!("alias {alias} of {}", claim(&name, &asset)),
}),
None => {
slot.aliases.insert(alias.clone(), name.clone());
}
}
}
slot.assets.insert(name, Arc::new(asset));
collisions
}
pub fn remove(&mut self, layer: Layer, name: &str) -> Option<Arc<MicroAsset>> {
let slot = &mut self.layers[layer.index()];
let removed = slot.assets.remove(name)?;
slot.aliases.retain(|_, target| target != name);
Some(removed)
}
pub fn get(&self, layer: Layer, name: &str) -> Option<&Arc<MicroAsset>> {
let slot = &self.layers[layer.index()];
slot.assets.get(name).or_else(|| {
slot.aliases
.get(name)
.and_then(|target| slot.assets.get(target))
})
}
pub fn resolve(&self, name: &str) -> Option<&Arc<MicroAsset>> {
Layer::ALL
.iter()
.rev()
.find_map(|layer| self.get(*layer, name))
}
pub fn require(&self, name: &str) -> Result<&Arc<MicroAsset>, MicroError> {
check_name(name)?;
self.resolve(name)
.ok_or_else(|| MicroError::UnknownAsset(name.to_string()))
}
pub fn shadowed(&self, name: &str) -> Vec<&Arc<MicroAsset>> {
let mut claims = Layer::ALL
.iter()
.rev()
.filter_map(|layer| self.get(*layer, name));
claims.next();
claims.collect()
}
pub fn names(&self) -> Vec<&str> {
let mut names: Vec<&str> = self
.layers
.iter()
.flat_map(|slot| slot.assets.keys().map(String::as_str))
.collect();
names.sort_unstable();
names.dedup();
names
}
pub fn assets(&self) -> Vec<&Arc<MicroAsset>> {
self.names()
.into_iter()
.filter_map(|name| self.resolve(name))
.collect()
}
pub fn layer(&self, layer: Layer) -> impl Iterator<Item = &Arc<MicroAsset>> {
self.layers[layer.index()].assets.values()
}
pub fn precedence(&self) -> Precedence {
let mut precedence = Precedence::new();
for layer in Layer::ALL {
let slot = &self.layers[layer.index()];
let mut entry = PrecedenceLayer::new(layer.as_str());
if let Some(origin) = &slot.origin {
entry = entry.origin(rich_ext::sanitize_terminal_controls(origin));
}
let mut values: BTreeMap<&str, String> = BTreeMap::new();
for (name, asset) in &slot.assets {
values.insert(name, describe(asset));
}
for (alias, target) in &slot.aliases {
values.insert(alias, format!("alias of {target}"));
}
for (key, value) in values {
entry = entry.value(key, value);
}
precedence = precedence.layer(entry);
}
precedence
}
pub fn explain(&self, name: &str) -> Option<Explanation> {
self.precedence().explain(name)
}
}
fn describe(asset: &MicroAsset) -> String {
let mut out = format!("{} {}", asset.name(), asset.size());
if let Some(version) = asset.version() {
out.push_str(&format!(" v{version}"));
}
if let Some(pack) = &asset.origin().pack {
out.push_str(&format!(" (pack {pack})"));
}
out
}