rust_relations_explorer/utils/
mod.rs1pub mod table {
3 fn sep(widths: &[usize]) -> String {
5 let mut s = String::from("+");
6 for w in widths {
7 s.push_str(&"-".repeat(w + 2));
8 s.push('+');
9 }
10 s
11 }
12
13 fn line(cells: &[String], widths: &[usize]) -> String {
15 let mut s = String::from("|");
16 for (i, cell) in cells.iter().enumerate() {
17 let w = widths[i];
18 s.push(' ');
19 s.push_str(cell);
20 if cell.len() < w {
21 s.push_str(&" ".repeat(w - cell.len()));
22 }
23 s.push(' ');
24 s.push('|');
25 }
26 s
27 }
28
29 pub fn render(headers: &[&str], rows: &[Vec<String>]) -> String {
31 let cols = headers.len();
32 let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
33 for row in rows {
34 for (c, w) in widths.iter_mut().enumerate().take(cols) {
35 *w = (*w).max(row.get(c).map_or(0, String::len));
36 }
37 }
38
39 let mut out = String::new();
40 out.push_str(&sep(&widths));
41 out.push('\n');
42 let header_cells: Vec<String> = headers.iter().map(|s| (*s).to_string()).collect();
43 out.push_str(&line(&header_cells, &widths));
44 out.push('\n');
45 out.push_str(&sep(&widths));
46 out.push('\n');
47 for row in rows {
48 let mut cells = Vec::with_capacity(cols);
49 for i in 0..cols {
50 cells.push(row.get(i).cloned().unwrap_or_default());
51 }
52 out.push_str(&line(&cells, &widths));
53 out.push('\n');
54 }
55 out.push_str(&sep(&widths));
56 out
57 }
58}
59
60pub mod config {
61 use serde::Deserialize;
62 use std::fs;
63 use std::path::{Path, PathBuf};
64
65 #[derive(Debug, Clone, Deserialize, Default)]
66 pub struct DotConfig {
67 pub clusters: Option<bool>,
68 pub legend: Option<bool>,
69 pub theme: Option<String>, pub rankdir: Option<String>, pub splines: Option<String>, pub rounded: Option<bool>,
73 }
74
75 #[derive(Debug, Clone, Deserialize, Default)]
76 pub struct SvgConfig {
77 pub interactive: Option<bool>,
78 }
79
80 #[derive(Debug, Clone, Deserialize, Default)]
81 pub struct QueryConfig {
82 pub default_format: Option<String>, }
84
85 #[derive(Debug, Clone, Deserialize, Default)]
86 pub struct Config {
87 pub root: Option<String>,
88 pub dot: Option<DotConfig>,
89 pub svg: Option<SvgConfig>,
90 pub query: Option<QueryConfig>,
91 }
92
93 fn default_config_path(root: &Path) -> PathBuf {
94 root.join("rust-relations-explorer.toml")
96 }
97
98 #[must_use]
99 pub fn load_config_at(path: &Path) -> Option<Config> {
100 let data = fs::read_to_string(path).ok()?;
101 toml::from_str::<Config>(&data).ok()
102 }
103
104 #[must_use]
105 pub fn load_config_near(root: &Path) -> Option<Config> {
106 let p_new = default_config_path(root);
107 if p_new.exists() {
108 return load_config_at(&p_new);
109 }
110 let p_old = root.join("knowledge-rs.toml");
111 if p_old.exists() {
112 load_config_at(&p_old)
113 } else {
114 None
115 }
116 }
117}
118
119pub mod cache {
120 use serde::{Deserialize, Serialize};
121 use std::collections::HashMap;
122 use std::path::{Path, PathBuf};
123
124 use crate::graph::FileNode;
125
126 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
127 pub struct CacheEntryMeta {
128 pub mtime: u64,
129 pub len: u64,
130 }
131
132 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
133 pub struct CacheEntry {
134 pub meta: CacheEntryMeta,
135 pub node: FileNode,
136 }
137
138 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
139 pub struct Cache {
140 pub entries: HashMap<PathBuf, CacheEntry>,
141 }
142
143 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
144 pub enum CacheMode {
145 Use,
146 Ignore,
147 Rebuild,
148 }
149
150 fn cache_path(root: &Path) -> PathBuf {
151 root.join(".knowledge_cache.json")
152 }
153
154 #[must_use]
155 pub fn load_cache(root: &Path) -> Option<Cache> {
156 let path = cache_path(root);
157 let data = std::fs::read_to_string(path).ok()?;
158 serde_json::from_str::<Cache>(&data).ok()
159 }
160
161 pub fn save_cache(root: &Path, cache: &Cache) {
162 let path = cache_path(root);
163 if let Ok(data) = serde_json::to_string_pretty(cache) {
164 let _ = std::fs::write(path, data);
165 }
166 }
167
168 pub fn clear_cache(root: &Path) {
169 let path = cache_path(root);
170 let _ = std::fs::remove_file(path);
171 }
172}
173
174pub mod file_walker {
175 #[must_use]
177 pub fn rust_files_with_options(root: &str, no_ignore: bool) -> Vec<String> {
178 let mut out = Vec::new();
179 let mut walker = ignore::WalkBuilder::new(root);
180 walker
182 .follow_links(false)
183 .git_ignore(!no_ignore)
184 .git_global(false)
185 .git_exclude(false)
186 .ignore(!no_ignore)
187 .parents(true);
188 let root_path = std::path::Path::new(root);
190 let matcher = if no_ignore {
191 None
192 } else {
193 let mut gi_builder = ignore::gitignore::GitignoreBuilder::new(root_path);
194 let gi = root_path.join(".gitignore");
195 if gi.exists() {
196 let _ = gi_builder.add(gi);
197 }
198 let ign = root_path.join(".ignore");
199 if ign.exists() {
200 let _ = gi_builder.add(ign);
201 }
202 gi_builder.build().ok()
203 };
204 for entry in walker.build().flatten() {
205 if entry.file_type().is_some_and(|t| t.is_file()) {
206 if let Some(m) = &matcher {
208 if m.matched(entry.path(), false).is_ignore() {
209 continue;
210 }
211 }
212 if entry.path().extension() == Some(std::ffi::OsStr::new("rs")) {
213 if let Some(s) = entry.path().to_str() {
214 out.push(s.to_string());
215 }
216 }
217 }
218 }
219 out
220 }
221
222 #[must_use]
224 pub fn rust_files(root: &str) -> Vec<String> {
225 let no_ignore = std::env::var("KNOWLEDGE_RS_NO_IGNORE")
226 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
227 .unwrap_or(false);
228 rust_files_with_options(root, no_ignore)
229 }
230}