rustbrain_core/
registry.rs1use crate::error::{BrainError, Result};
8use crate::query::{QueryOptions, RankedHit};
9use crate::storage::Database;
10use crate::types::Node;
11use serde::{Deserialize, Serialize};
12use std::fs;
13use std::path::{Path, PathBuf};
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
20pub struct GlobalRegistry {
21 #[serde(default)]
23 pub workspaces: Vec<String>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct GlobalRankedHit {
29 pub workspace: String,
31 pub hit: RankedHit,
33}
34
35impl GlobalRegistry {
36 pub fn config_path() -> Result<PathBuf> {
38 let base = dirs::config_dir()
39 .ok_or_else(|| BrainError::other("could not determine user config directory"))?;
40 let dir = base.join("rustbrain");
41 fs::create_dir_all(&dir)?;
42 Ok(dir.join("registry.json"))
43 }
44
45 pub fn load() -> Result<Self> {
47 let path = Self::config_path()?;
48 if !path.exists() {
49 return Ok(Self::default());
50 }
51 let content = fs::read_to_string(&path)?;
52 let mut reg: Self = serde_json::from_str(&content).unwrap_or_default();
53 reg.prune_missing();
54 Ok(reg)
55 }
56
57 pub fn save(&self) -> Result<()> {
59 let path = Self::config_path()?;
60 let tmp = path.with_extension("json.tmp");
61 let json = serde_json::to_string_pretty(self)?;
62 fs::write(&tmp, json)?;
63 fs::rename(&tmp, &path)?;
64 Ok(())
65 }
66
67 pub fn prune_missing(&mut self) {
69 self.workspaces
70 .retain(|ws| Path::new(ws).join(".brain").join("db.sqlite").exists());
71 }
72
73 pub fn register<P: AsRef<Path>>(&mut self, workspace_path: P) -> Result<()> {
75 let abs_path = fs::canonicalize(workspace_path.as_ref())
76 .unwrap_or_else(|_| workspace_path.as_ref().to_path_buf())
77 .to_string_lossy()
78 .to_string();
79
80 if !self.workspaces.contains(&abs_path) {
81 self.workspaces.push(abs_path);
82 self.save()?;
83 }
84 Ok(())
85 }
86
87 pub fn search_all(&self, query: &str) -> Result<Vec<(String, Vec<Node>)>> {
89 let ranked = self.search_all_ranked(query, &QueryOptions::default())?;
90 let mut by_ws: std::collections::BTreeMap<String, Vec<Node>> =
91 std::collections::BTreeMap::new();
92 for gh in ranked {
93 by_ws.entry(gh.workspace).or_default().push(gh.hit.node);
94 }
95 Ok(by_ws.into_iter().collect())
96 }
97
98 pub fn search_all_ranked(
100 &self,
101 query: &str,
102 opts: &QueryOptions,
103 ) -> Result<Vec<GlobalRankedHit>> {
104 let mut merged: Vec<GlobalRankedHit> = Vec::new();
105
106 for ws in &self.workspaces {
107 let db_path = PathBuf::from(ws).join(".brain").join("db.sqlite");
108 if !db_path.exists() {
109 continue;
110 }
111 match Database::open(&db_path) {
112 Ok(db) => match db.search_ranked(query, opts) {
113 Ok(hits) => {
114 for hit in hits {
115 merged.push(GlobalRankedHit {
116 workspace: ws.clone(),
117 hit,
118 });
119 }
120 }
121 Err(e) => {
122 eprintln!("rustbrain: skip workspace {ws}: {e}");
123 }
124 },
125 Err(e) => {
126 eprintln!("rustbrain: cannot open {ws}: {e}");
127 }
128 }
129 }
130
131 merged.sort_by(|a, b| {
132 b.hit
133 .score
134 .partial_cmp(&a.hit.score)
135 .unwrap_or(std::cmp::Ordering::Equal)
136 });
137 merged.truncate(opts.limit);
138 Ok(merged)
139 }
140}