use std::collections::BTreeMap;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct IconGallery {
#[serde(default)]
pub kinds: BTreeMap<String, String>,
#[serde(default)]
pub vendors: BTreeMap<String, String>,
#[serde(default)]
pub resources: BTreeMap<String, String>,
}
fn norm(s: &str) -> String {
s.trim().to_lowercase()
}
impl IconGallery {
pub fn builtin() -> Self {
let kinds = [
("service", "service"),
("server", "service"),
("repo", "repo"),
("repository", "repo"),
("table", "table"),
("column", "column"),
("database", "database"),
("schema", "schema"),
("person", "person"),
("user", "person"),
("team", "person"),
("file", "file"),
("directory", "directory"),
("config", "config"),
("secret", "secret"),
("credential", "secret"),
("concept", "concept"),
("namespace", "namespace"),
("deployment", "deployment"),
("pod", "pod"),
("infra", "infra"),
("infracomponent", "infra"),
("endpoint", "endpoint"),
("api", "api"),
("function", "function"),
("class", "class"),
("module", "module"),
("memory", "memory"),
("fact", "memory"),
("decision", "concept"),
("issue", "issue"),
("pullrequest", "pullrequest"),
("organization", "organization"),
];
let vendors = [
("github", "github"),
("gitlab", "gitlab"),
("bitbucket", "bitbucket"),
("slack", "slack"),
("datadog", "datadog"),
("kubernetes", "kubernetes"),
("k8s", "kubernetes"),
("docker", "docker"),
("grafana", "grafana"),
("pagerduty", "pagerduty"),
("gcp", "gcp"),
("googlecloud", "gcp"),
("aws", "aws"),
("amazon", "aws"),
("s3", "s3"),
("postgres", "postgresql"),
("postgresql", "postgresql"),
("prometheus", "prometheus"),
("sentry", "sentry"),
("opentelemetry", "opentelemetry"),
("otel", "opentelemetry"),
("redis", "redis"),
("mongodb", "mongodb"),
("snowflake", "snowflake"),
("elastic", "elastic"),
("elasticsearch", "elastic"),
("terraform", "terraform"),
("jira", "jira"),
("linear", "linear"),
("asana", "asana"),
("notion", "notion"),
("confluence", "confluence"),
("googledrive", "googledrive"),
("gmail", "gmail"),
];
let resources = [
("pod", "pod"),
("container", "pod"),
("deployment", "deployment"),
("replicaset", "deployment"),
("statefulset", "deployment"),
("daemonset", "deployment"),
("service", "service"),
("svc", "service"),
("ingress", "endpoint"),
("node", "infra"),
("cluster", "infra"),
("namespace", "namespace"),
("configmap", "config"),
("secret", "secret"),
("instance", "service"),
("vm", "service"),
("ec2", "service"),
("lambda", "function"),
("function", "function"),
("bucket", "database"),
("s3", "database"),
("volume", "database"),
("disk", "database"),
("database", "database"),
("db", "database"),
("rds", "database"),
("table", "table"),
("queue", "module"),
("topic", "module"),
("stream", "module"),
("repo", "repo"),
("repository", "repo"),
("monitor", "service"),
("dashboard", "concept"),
("alert", "issue"),
];
Self {
kinds: kinds.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
vendors: vendors.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
resources: resources.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
}
}
pub fn load() -> Self {
let mut g = Self::builtin();
if let Ok(path) = std::env::var("PENSIEVE_ICON_GALLERY") {
match std::fs::read_to_string(&path) {
Ok(s) => match serde_json::from_str::<IconGallery>(&s) {
Ok(extra) => {
for (k, v) in extra.kinds {
g.kinds.insert(norm(&k), v);
}
for (k, v) in extra.vendors {
g.vendors.insert(norm(&k), v);
}
for (k, v) in extra.resources {
g.resources.insert(norm(&k), v);
}
}
Err(e) => tracing::warn!("PENSIEVE_ICON_GALLERY parse failed: {e}"),
},
Err(e) => tracing::warn!("PENSIEVE_ICON_GALLERY read failed ({path}): {e}"),
}
}
g
}
pub fn global() -> &'static IconGallery {
static G: OnceLock<IconGallery> = OnceLock::new();
G.get_or_init(IconGallery::load)
}
pub fn resolve(&self, kind: Option<&str>, vendor: Option<&str>) -> Option<String> {
if let Some(v) = vendor.map(norm).filter(|s| !s.is_empty()) {
if let Some(icon) = self.vendors.get(&v) {
return Some(icon.clone());
}
}
if let Some(k) = kind.map(norm).filter(|s| !s.is_empty()) {
if let Some(icon) = self.kinds.get(&k) {
return Some(icon.clone());
}
}
None
}
pub fn resolve_type(&self, type_str: &str) -> Option<String> {
let parts: Vec<String> = type_str
.split(|c| c == ':' || c == '/')
.map(norm)
.filter(|s| !s.is_empty())
.collect();
if let Some(provider) = parts.first() {
if let Some(icon) = self.vendors.get(provider) {
return Some(icon.clone());
}
}
if let Some(resource) = parts.last() {
if let Some(icon) = self.resources.get(resource) {
return Some(icon.clone());
}
if let Some(icon) = self.kinds.get(resource) {
return Some(icon.clone());
}
}
None
}
pub fn catalog_hint(&self) -> String {
let mut kinds: Vec<&str> = self.kinds.values().map(String::as_str).collect();
kinds.sort_unstable();
kinds.dedup();
let mut brands: Vec<&str> = self.vendors.values().map(String::as_str).collect();
brands.sort_unstable();
brands.dedup();
format!("kinds: {} · brands: {}", kinds.join(", "), brands.join(", "))
}
}