use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::project_root::{read_project_pin, PIN_FILE_REL};
pub fn default_search_dirs() -> Vec<PathBuf> {
let Some(home) = dirs::home_dir() else {
return Vec::new();
};
vec![
home.join("Projects"),
home.join("Developer"),
home.join("Code"),
home.clone(),
]
}
pub fn scan_pin_map(search_dirs: &[PathBuf]) -> HashMap<String, PathBuf> {
let mut map: HashMap<String, PathBuf> = HashMap::new();
for search_dir in search_dirs {
scan_one_root(search_dir, &mut map);
}
map
}
fn scan_one_root(search_dir: &Path, map: &mut HashMap<String, PathBuf>) {
let entries = match std::fs::read_dir(search_dir) {
Ok(e) => e,
Err(e) => {
tracing::debug!(
dir = %search_dir.display(),
"startup scan: cannot read dir ({e}); skipping"
);
return;
}
};
for entry in entries.flatten() {
let candidate = entry.path();
if !candidate.is_dir() {
continue;
}
let pin = match read_project_pin(&candidate) {
Ok(Some(p)) => p,
Ok(None) => continue, Err(e) => {
tracing::debug!(
path = %candidate.join(PIN_FILE_REL).display(),
"startup scan: skipping unreadable/corrupt pin file ({e})"
);
continue;
}
};
let palace_id = pin.palace;
if let Some(existing) = map.get(&palace_id) {
tracing::warn!(
palace_id = %palace_id,
first = %existing.display(),
second = %candidate.display(),
"startup scan: duplicate palace id claimed by two projects; keeping first"
);
} else {
tracing::debug!(
palace_id = %palace_id,
project = %candidate.display(),
"startup scan: discovered pin"
);
map.insert(palace_id, candidate);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::project_root::{write_project_pin, ProjectPin, PIN_SCHEMA_VERSION};
use std::fs;
fn write_pin(project_dir: &Path, palace_id: &str) {
let pin = ProjectPin {
schema_version: PIN_SCHEMA_VERSION,
palace: palace_id.to_string(),
note: None,
};
write_project_pin(project_dir, &pin).expect("write_pin test helper");
}
#[test]
fn scan_pin_map_two_pins_found() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let alpha_dir = root.join("alpha-proj");
let beta_dir = root.join("beta-proj");
fs::create_dir_all(&alpha_dir).unwrap();
fs::create_dir_all(&beta_dir).unwrap();
write_pin(&alpha_dir, "alpha");
write_pin(&beta_dir, "beta");
let no_pin_dir = root.join("no-pin-proj");
fs::create_dir_all(&no_pin_dir).unwrap();
let map = scan_pin_map(&[root.to_path_buf()]);
assert_eq!(map.len(), 2, "expected exactly 2 entries; got: {map:?}");
let actual_alpha = fs::canonicalize(map.get("alpha").expect("alpha")).unwrap();
let actual_beta = fs::canonicalize(map.get("beta").expect("beta")).unwrap();
let expected_alpha = fs::canonicalize(&alpha_dir).unwrap();
let expected_beta = fs::canonicalize(&beta_dir).unwrap();
assert_eq!(actual_alpha, expected_alpha);
assert_eq!(actual_beta, expected_beta);
}
#[test]
fn scan_pin_map_skips_corrupt_yaml() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let good_dir = root.join("good-proj");
let bad_dir = root.join("bad-proj");
fs::create_dir_all(&good_dir).unwrap();
fs::create_dir_all(&bad_dir).unwrap();
write_pin(&good_dir, "good-palace");
let trusty_dir = bad_dir.join(".trusty-tools");
fs::create_dir_all(&trusty_dir).unwrap();
fs::write(
trusty_dir.join("trusty-memory.yaml"),
b"palace: \x00\nbroken: [",
)
.unwrap();
let map = scan_pin_map(&[root.to_path_buf()]);
assert_eq!(map.len(), 1, "only the valid pin must be recorded");
assert!(
map.contains_key("good-palace"),
"good-palace must be present"
);
assert!(
!map.values().any(|p| p.ends_with("bad-proj")),
"bad-proj must not appear in the map"
);
}
#[test]
fn scan_pin_map_one_level_only() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let outer = root.join("outer-proj");
let inner = outer.join("inner-proj");
fs::create_dir_all(&inner).unwrap();
write_pin(&inner, "deep-palace");
let map = scan_pin_map(&[root.to_path_buf()]);
assert!(
map.is_empty(),
"two-level-deep pin must not be discovered; got: {map:?}"
);
}
#[test]
fn scan_pin_map_missing_root_dir_contributes_nothing() {
let map = scan_pin_map(&[PathBuf::from("/tmp/nonexistent-trusty-scan-test-dir-xyz")]);
assert!(
map.is_empty(),
"missing root must yield empty map; got: {map:?}"
);
}
#[test]
fn scan_pin_map_empty_search_dirs() {
let map = scan_pin_map(&[]);
assert!(map.is_empty());
}
#[test]
fn scan_pin_map_accumulates_across_roots() {
let tmp = tempfile::tempdir().expect("tempdir");
let root1 = tmp.path().join("root1");
let root2 = tmp.path().join("root2");
let proj1 = root1.join("proj-one");
let proj2 = root2.join("proj-two");
fs::create_dir_all(&proj1).unwrap();
fs::create_dir_all(&proj2).unwrap();
write_pin(&proj1, "palace-one");
write_pin(&proj2, "palace-two");
let map = scan_pin_map(&[root1, root2]);
assert_eq!(map.len(), 2);
assert!(map.contains_key("palace-one"));
assert!(map.contains_key("palace-two"));
}
}