use crate::packages::Kind;
use anyhow::Result;
use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Requirement {
pub kind: Kind,
pub name: String,
pub source: String,
}
pub fn scan(root: &Path) -> Result<Vec<Requirement>> {
let mut reqs = Vec::new();
git_config(root, &mut reqs)?;
alacritty(root, &mut reqs)?;
zed(root, &mut reqs)?;
zsh(root, &mut reqs)?;
config_dirs(root, &mut reqs)?;
reqs.sort_by(|a, b| (a.kind.label(), &a.name).cmp(&(b.kind.label(), &b.name)));
reqs.dedup_by(|a, b| a.kind == b.kind && a.name == b.name);
Ok(reqs)
}
fn read(root: &Path, rel: &str) -> Option<String> {
std::fs::read_to_string(root.join(rel)).ok()
}
fn command_name(value: &str) -> Option<String> {
let v = value.trim().trim_start_matches('!').trim();
let first = v.split_whitespace().next()?;
let name = first.rsplit('/').next()?;
if name.is_empty() {
None
} else {
Some(name.to_string())
}
}
fn git_config(root: &Path, out: &mut Vec<Requirement>) -> Result<()> {
let Some(text) = read(root, ".config/git/config") else {
return Ok(());
};
for line in text.lines() {
let line = line.trim();
let Some((key, value)) = line.split_once('=') else {
continue;
};
let key = key.trim();
if !matches!(key, "pager" | "diffFilter" | "helper") {
continue;
}
if let Some(name) = command_name(value) {
out.push(Requirement {
kind: Kind::Command,
name,
source: format!(".config/git/config ({key})"),
});
}
}
Ok(())
}
fn alacritty(root: &Path, out: &mut Vec<Requirement>) -> Result<()> {
let Some(text) = read(root, ".config/alacritty/alacritty.toml") else {
return Ok(());
};
for cap in find_all(&text, "family = \"") {
out.push(Requirement {
kind: Kind::Font,
name: cap,
source: ".config/alacritty/alacritty.toml (font.family)".into(),
});
}
Ok(())
}
fn zed(root: &Path, out: &mut Vec<Requirement>) -> Result<()> {
let Some(text) = read(root, ".config/zed/settings.json") else {
return Ok(());
};
for key in ["\"buffer_font_family\": \"", "\"font_family\": \""] {
for cap in find_all(&text, key) {
out.push(Requirement {
kind: Kind::Font,
name: cap,
source: ".config/zed/settings.json (font_family)".into(),
});
}
}
if let Some(start) = text.find("\"auto_install_extensions\"") {
if let Some(open) = text[start..].find('{') {
let rest = &text[start + open..];
if let Some(close) = rest.find('}') {
for cap in find_all(&rest[..close], "\"") {
if !cap.is_empty() {
out.push(Requirement {
kind: Kind::Extension,
name: cap,
source: ".config/zed/settings.json (auto_install_extensions)".into(),
});
}
}
}
}
}
Ok(())
}
fn zsh(root: &Path, out: &mut Vec<Requirement>) -> Result<()> {
let mut files: Vec<std::path::PathBuf> = vec![root.join(".zshenv")];
if let Ok(dir) = std::fs::read_dir(root.join(".config/zsh/rc")) {
files.extend(dir.filter_map(|e| e.ok()).map(|e| e.path()));
}
files.push(root.join(".config/zsh/.zshrc"));
for path in files {
let Ok(text) = std::fs::read_to_string(&path) else {
continue;
};
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.display()
.to_string();
for cap in find_all(&text, "eval \"$(") {
if let Some(name) = command_name(&cap) {
out.push(Requirement {
kind: Kind::Command,
name,
source: format!("{rel} (eval)"),
});
}
}
for cap in find_all(&text, "${+commands[") {
let name = cap.trim_end_matches(']').to_string();
if !name.is_empty() {
out.push(Requirement {
kind: Kind::Command,
name,
source: format!("{rel} (commands[])"),
});
}
}
}
Ok(())
}
fn config_dirs(root: &Path, out: &mut Vec<Requirement>) -> Result<()> {
let Ok(dir) = std::fs::read_dir(root.join(".config")) else {
return Ok(());
};
for entry in dir.filter_map(|e| e.ok()) {
let name = entry.file_name().to_string_lossy().to_string();
if name.ends_with(".tmpl") {
continue;
}
let name = name.strip_suffix(".toml").unwrap_or(&name).to_string();
if name.starts_with('.') {
continue;
}
out.push(Requirement {
kind: Kind::Command,
name,
source: ".config/ (directory exists)".into(),
});
}
Ok(())
}
fn find_all(text: &str, prefix: &str) -> Vec<String> {
let close = match prefix.chars().last() {
Some('"') => '"',
Some('(') => ')',
Some('[') => ']',
_ => return Vec::new(),
};
let mut out = Vec::new();
let mut rest = text;
while let Some(i) = rest.find(prefix) {
let after = &rest[i + prefix.len()..];
if let Some(j) = after.find(close) {
out.push(after[..j].to_string());
rest = &after[j..];
} else {
break;
}
}
out
}