use crate::config::Config;
use ignore::WalkBuilder;
use std::{collections::BTreeSet, path::PathBuf};
pub fn discover(cfg: &Config) -> Vec<PathBuf> {
let mut paths = BTreeSet::new();
for dir in known_dirs() {
add_glob(&mut paths, dir);
}
for dir in &cfg.paths.include {
add_glob(&mut paths, expand_home(dir));
}
paths
.into_iter()
.filter(|path| !cfg.paths.exclude.iter().any(|x| path.starts_with(x)))
.collect()
}
pub fn scan(include: &[PathBuf]) -> Vec<PathBuf> {
let Some(home) = dirs::home_dir() else {
return Vec::new();
};
let mut paths = BTreeSet::new();
scan_root(&mut paths, home, Some(5));
for path in include {
scan_root(&mut paths, expand_home(path), None);
}
paths.into_iter().collect()
}
fn scan_root(paths: &mut BTreeSet<PathBuf>, root: PathBuf, max_depth: Option<usize>) {
if root.is_file() {
if is_db(&root) {
paths.insert(root);
}
return;
}
let mut walk = WalkBuilder::new(root);
walk.hidden(false).ignore(false).git_ignore(false);
if let Some(depth) = max_depth {
walk.max_depth(Some(depth));
}
for entry in walk.build().filter_map(Result::ok) {
let path = entry.path();
if is_db(path) {
paths.insert(path.to_path_buf());
}
}
}
fn known_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Some(dir) = dirs::data_dir() {
dirs.push(dir.join("opencode"));
}
if let Some(home) = dirs::home_dir() {
dirs.push(home.join(".local/share/opencode"));
if cfg!(target_os = "macos") {
dirs.push(home.join("Library/Application Support/opencode"));
dirs.push(home.join("Library/Mobile Documents"));
dirs.push(home.join("Library/CloudStorage"));
}
if cfg!(target_os = "windows") {
dirs.push(home.join("AppData/Local/opencode"));
dirs.push(home.join("AppData/Roaming/opencode"));
dirs.push(home.join("OneDrive"));
}
if cfg!(target_os = "linux") {
dirs.push(home.join(".var/app"));
}
}
dirs
}
fn add_glob(paths: &mut BTreeSet<PathBuf>, dir: PathBuf) {
if dir.is_file() && is_db(&dir) {
paths.insert(dir);
return;
}
let Ok(items) = std::fs::read_dir(&dir) else {
return;
};
for item in items.filter_map(Result::ok) {
let path = item.path();
if path.is_file() && is_db(&path) {
paths.insert(path);
}
}
}
fn is_db(path: &std::path::Path) -> bool {
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
name.starts_with("opencode") && name.ends_with(".db")
}
fn expand_home(path: &std::path::Path) -> PathBuf {
let Some(text) = path.to_str() else {
return path.to_path_buf();
};
if text == "~" {
return dirs::home_dir().unwrap_or_else(|| path.to_path_buf());
}
let Some(rest) = text.strip_prefix("~/") else {
return path.to_path_buf();
};
dirs::home_dir()
.map(|home| home.join(rest))
.unwrap_or_else(|| path.to_path_buf())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn expands_quoted_home_paths() {
let Some(home) = dirs::home_dir() else {
return;
};
assert_eq!(
expand_home(std::path::Path::new("~/work")),
home.join("work")
);
}
}