use std::path::PathBuf;
pub fn home_roots() -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = Vec::new();
if let Some(h) = dirs::home_dir() {
out.push(h);
}
if is_wsl() {
if let Ok(rd) = std::fs::read_dir("/mnt/c/Users") {
for ent in rd.flatten() {
let p = ent.path();
if !p.is_dir() { continue; }
let name = ent.file_name();
let lname = name.to_string_lossy().to_ascii_lowercase();
if matches!(lname.as_str(),
"public" | "default" | "default user" | "all users"
| "defaultuser0" | "defaultuser100000" | "desktop.ini"
) { continue; }
out.push(p);
}
}
}
#[cfg(windows)]
{
for h in wsl_homes_from_windows() { out.push(h); }
}
if let Ok(s) = std::env::var("AGTOP_EXTRA_HOMES") {
let sep = if cfg!(windows) { ';' } else { ':' };
for tok in s.split(sep) {
let t = tok.trim();
if t.is_empty() { continue; }
out.push(PathBuf::from(t));
}
}
let mut seen = std::collections::HashSet::new();
out.retain(|p| seen.insert(p.clone()));
out
}
#[cfg(windows)]
fn wsl_homes_from_windows() -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = Vec::new();
let distros = list_wsl_distros();
for share in [r"\\wsl$", r"\\wsl.localhost"] {
for distro in &distros {
let home_root = PathBuf::from(format!(r"{share}\{distro}\home"));
if let Ok(rd) = std::fs::read_dir(&home_root) {
for ent in rd.flatten() {
let p = ent.path();
if p.is_dir() { out.push(p); }
}
}
let root_home = PathBuf::from(format!(r"{share}\{distro}\root"));
if root_home.exists() { out.push(root_home); }
}
if !out.is_empty() { break; }
}
out
}
#[cfg(windows)]
fn list_wsl_distros() -> Vec<String> {
for share in [r"\\wsl$\", r"\\wsl.localhost\"] {
if let Ok(rd) = std::fs::read_dir(share) {
let names: Vec<String> = rd.flatten()
.filter_map(|e| e.file_name().to_str().map(String::from))
.filter(|n| !n.is_empty() && !n.starts_with('.'))
.collect();
if !names.is_empty() { return names; }
}
}
let output = match std::process::Command::new("wsl.exe")
.args(["-l", "-q"])
.output() {
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
let bytes = output.stdout;
let start = if bytes.starts_with(&[0xFF, 0xFE]) { 2 } else { 0 };
let u16s: Vec<u16> = bytes[start..].chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let s = String::from_utf16_lossy(&u16s);
s.lines()
.map(|l| l.trim().trim_matches('\0').to_string())
.filter(|l| !l.is_empty())
.collect()
}
#[cfg(target_os = "linux")]
fn is_wsl() -> bool {
if let Ok(s) = std::fs::read_to_string("/proc/version") {
let l = s.to_ascii_lowercase();
if l.contains("microsoft") || l.contains("wsl") { return true; }
}
if let Ok(s) = std::fs::read_to_string("/proc/sys/kernel/osrelease") {
let l = s.to_ascii_lowercase();
if l.contains("microsoft") || l.contains("wsl") { return true; }
}
false
}
#[cfg(not(target_os = "linux"))]
fn is_wsl() -> bool { false }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn own_home_always_first() {
let r = home_roots();
if let Some(h) = dirs::home_dir() {
assert_eq!(r.first(), Some(&h));
}
}
#[test]
fn extra_homes_env_appended() {
let sep = if cfg!(windows) { ";" } else { ":" };
let val = format!("/tmp/fake-home-a{sep}/tmp/fake-home-b");
let prev = std::env::var("AGTOP_EXTRA_HOMES").ok();
std::env::set_var("AGTOP_EXTRA_HOMES", &val);
let r = home_roots();
assert!(r.iter().any(|p| p == &PathBuf::from("/tmp/fake-home-a")));
assert!(r.iter().any(|p| p == &PathBuf::from("/tmp/fake-home-b")));
match prev {
Some(v) => std::env::set_var("AGTOP_EXTRA_HOMES", v),
None => std::env::remove_var("AGTOP_EXTRA_HOMES"),
}
}
}