use std::collections::HashSet;
use std::path::{Path, PathBuf};
#[must_use]
pub fn scan_plugin_dirs(config_dirs: &[PathBuf]) -> Vec<PathBuf> {
scan_dirs(config_dirs, xdg_segments_dir().as_deref())
}
pub(crate) fn scan_dirs(config_dirs: &[PathBuf], xdg_dir: Option<&Path>) -> Vec<PathBuf> {
let mut ordered: Vec<&Path> = config_dirs.iter().map(PathBuf::as_path).collect();
if let Some(p) = xdg_dir {
ordered.push(p);
}
let mut results = Vec::new();
let mut seen: HashSet<PathBuf> = HashSet::new();
for dir in ordered {
for path in list_rhai_files(dir) {
let key = std::fs::canonicalize(&path).unwrap_or_else(|_| path.clone());
if seen.insert(key) {
results.push(path);
}
}
}
results
}
fn xdg_segments_dir() -> Option<PathBuf> {
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
if !xdg.is_empty() {
return Some(PathBuf::from(xdg).join("linesmith/segments"));
}
}
let home = std::env::var("HOME").ok().filter(|h| !h.is_empty())?;
Some(PathBuf::from(home).join(".config/linesmith/segments"))
}
fn list_rhai_files(dir: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut paths: Vec<PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| p.is_file() && p.extension().is_some_and(|ext| ext == "rhai"))
.collect();
paths.sort();
paths
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn touch_rhai(dir: &Path, name: &str) -> PathBuf {
let path = dir.join(name);
fs::write(&path, "fn render(ctx) {}\n").expect("write plugin fixture");
path
}
#[test]
fn missing_directory_returns_empty() {
let nonexistent = PathBuf::from("/does/not/exist/path/for/linesmith/test");
assert!(scan_dirs(&[nonexistent], None).is_empty());
}
#[test]
fn empty_directory_returns_empty() {
let tmp = TempDir::new().expect("tempdir");
assert!(scan_dirs(&[tmp.path().to_path_buf()], None).is_empty());
}
#[test]
fn finds_rhai_files_in_single_dir_sorted() {
let tmp = TempDir::new().expect("tempdir");
let b = touch_rhai(tmp.path(), "b.rhai");
let a = touch_rhai(tmp.path(), "a.rhai");
let found = scan_dirs(&[tmp.path().to_path_buf()], None);
assert_eq!(found, vec![a, b]);
}
#[test]
fn ignores_non_rhai_extensions() {
let tmp = TempDir::new().expect("tempdir");
fs::write(tmp.path().join("note.txt"), "not rhai").expect("write txt");
fs::write(tmp.path().join("script.rs"), "fn main(){}").expect("write rs");
let plugin = touch_rhai(tmp.path(), "real.rhai");
assert_eq!(scan_dirs(&[tmp.path().to_path_buf()], None), vec![plugin]);
}
#[test]
fn ignores_subdirectories_non_recursive() {
let tmp = TempDir::new().expect("tempdir");
let subdir = tmp.path().join("sub");
fs::create_dir(&subdir).expect("mkdir");
touch_rhai(&subdir, "nested.rhai");
let top = touch_rhai(tmp.path(), "top.rhai");
assert_eq!(scan_dirs(&[tmp.path().to_path_buf()], None), vec![top]);
}
#[test]
fn config_dirs_scanned_before_xdg_default() {
let cfg_dir = TempDir::new().expect("tempdir");
let xdg_dir = TempDir::new().expect("tempdir");
let cfg_plugin = touch_rhai(cfg_dir.path(), "from_config.rhai");
let xdg_plugin = touch_rhai(xdg_dir.path(), "from_xdg.rhai");
let found = scan_dirs(&[cfg_dir.path().to_path_buf()], Some(xdg_dir.path()));
assert_eq!(found, vec![cfg_plugin, xdg_plugin]);
}
#[test]
fn duplicate_path_across_dirs_deduped_by_canonical() {
let tmp = TempDir::new().expect("tempdir");
let plugin = touch_rhai(tmp.path(), "shared.rhai");
let dir = tmp.path().to_path_buf();
let found = scan_dirs(&[dir.clone(), dir], None);
assert_eq!(found, vec![plugin]);
}
#[test]
fn same_plugin_in_config_and_xdg_keeps_config_copy() {
let tmp = TempDir::new().expect("tempdir");
let plugin = touch_rhai(tmp.path(), "shared.rhai");
let found = scan_dirs(&[tmp.path().to_path_buf()], Some(tmp.path()));
assert_eq!(found, vec![plugin]);
}
#[test]
fn multiple_config_dirs_preserve_declaration_order() {
let a_dir = TempDir::new().expect("tempdir");
let b_dir = TempDir::new().expect("tempdir");
let a_plugin = touch_rhai(a_dir.path(), "from_a.rhai");
let b_plugin = touch_rhai(b_dir.path(), "from_b.rhai");
let found = scan_dirs(
&[a_dir.path().to_path_buf(), b_dir.path().to_path_buf()],
None,
);
assert_eq!(found, vec![a_plugin, b_plugin]);
}
#[test]
fn missing_xdg_dir_silently_skipped() {
let cfg_dir = TempDir::new().expect("tempdir");
let plugin = touch_rhai(cfg_dir.path(), "only.rhai");
let missing = PathBuf::from("/nonexistent/xdg/path");
let found = scan_dirs(&[cfg_dir.path().to_path_buf()], Some(&missing));
assert_eq!(found, vec![plugin]);
}
}