⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd
crates/apps/database_explorer/src/lib.rs
History
Who touched what
raw
chore: monorepo inicial con arje + minga + yahweh absorbidos
1//! `yahweh_database_explorer` — explorer de SQLite.
2//!
3//! Mismo patrón que `yahweh_file_explorer` pero con `SqliteProvider`. La
4//! UX es idéntica (TreeView con lazy load por chevron); cambia solo el
5//! origen de los datos: filas de una tabla `items(id, parent_id, name,
6//! display_type, content)` en lugar del filesystem.
7
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10
11use gpui::{
12 Context, Entity, EventEmitter, IntoElement, Render, SharedString, Window, div, prelude::*,
13 px,
14};
15
16use yahweh_core::{DataProvider, DisplayType, EntityNode};
17use yahweh_provider_sqlite::SqliteDataProvider;
18use yahweh_theme::Theme;
19use yahweh_widget_tree::{RowId, RowKind, TreeEvent, TreeRow, TreeView};
20
21#[derive(Clone, Debug)]
22#[allow(dead_code)] // Consumido por el AppBus en Fase 4+.
23pub enum DatabaseExplorerEvent {
24 EntitySelected { id: String },
25 EntityOpened { id: String },
26}
27
28pub struct DatabaseExplorer {
29 tree_view: Entity<TreeView>,
30 provider: Arc<SqliteDataProvider>,
31 db_path: String,
32
33 expanded: HashSet<String>,
34 children: HashMap<String, Vec<EntityNode>>,
35 pending: HashSet<String>,
36 /// Mensaje de error si la DB no abrió. Se muestra en el header.
37 open_error: Option<String>,
38}
39
40const ROOT_KEY: &str = "__db_root__";
41
42impl EventEmitter<DatabaseExplorerEvent> for DatabaseExplorer {}
43
44impl DatabaseExplorer {
45 /// `db_path` es la ruta al .sqlite. Si no existe se crea con la tabla
46 /// `items` y un seed mínimo (ver `SqliteDataProvider::new`).
47 pub fn new(db_path: String, cx: &mut Context<Self>) -> Self {
48 cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
49
50 let tree_view = cx.new(|cx| TreeView::new("db-explorer-tree", cx));
51 cx.subscribe(&tree_view, |this: &mut DatabaseExplorer, _, ev, cx| {
52 this.on_tree_event(ev, cx);
53 })
54 .detach();
55
56 let (provider, open_error) = match SqliteDataProvider::new(&db_path) {
57 Ok(p) => (Some(Arc::new(p)), None),
58 Err(e) => (None, Some(e)),
59 };
60
61 let mut expanded = HashSet::new();
62 expanded.insert(ROOT_KEY.to_string());
63
64 let mut me = Self {
65 tree_view,
66 // Usamos un dummy provider si la DB no abrió. La UI mostrará el
67 // error en el header; cualquier load_children retornará vacío.
68 provider: provider.unwrap_or_else(|| {
69 Arc::new(SqliteDataProvider::new(":memory:").expect("memory db"))
70 }),
71 db_path,
72 expanded,
73 children: HashMap::new(),
74 pending: HashSet::new(),
75 open_error,
76 };
77 // Cargar el root (parent_id NULL en SQLite, mapped to None acá).
78 me.load_children(ROOT_KEY.to_string(), cx);
79 me
80 }
81
82 pub fn db_path(&self) -> &str {
83 &self.db_path
84 }
85
86 fn load_children(&mut self, parent_key: String, cx: &mut Context<Self>) {
87 if self.pending.contains(&parent_key) || self.children.contains_key(&parent_key) {
88 return;
89 }
90 self.pending.insert(parent_key.clone());
91
92 let provider = self.provider.clone();
93 let parent_for_task = parent_key.clone();
94 cx.spawn(async move |this, cx| {
95 // ROOT_KEY → None; cualquier otro → Some(actual id).
96 let arg: Option<String> = if parent_for_task == ROOT_KEY {
97 None
98 } else {
99 Some(parent_for_task.clone())
100 };
101 let result = provider.list_children(arg.as_deref()).await;
102 let _ = this.update(cx, |this, cx| {
103 this.on_children_loaded(parent_for_task, result, cx);
104 });
105 })
106 .detach();
107 }
108
109 fn on_children_loaded(
110 &mut self,
111 parent_key: String,
112 result: Result<Vec<EntityNode>, String>,
113 cx: &mut Context<Self>,
114 ) {
115 self.pending.remove(&parent_key);
116 let mut entries = result.unwrap_or_default();
117 sort_entries(&mut entries);
118 self.children.insert(parent_key, entries);
119 self.push_rows(cx);
120 }
121
122 fn push_rows(&self, cx: &mut Context<Self>) {
123 let mut rows = Vec::new();
124 // Una row "raíz virtual" para que el árbol tenga un anchor visible.
125 rows.push(TreeRow {
126 id: RowId::new(ROOT_KEY),
127 label: format!("(db) {}", self.db_path),
128 depth: 0,
129 kind: RowKind::Branch,
130 expanded: self.expanded.contains(ROOT_KEY),
131 icon: Some("🗄️".to_string()),
132 });
133 if self.expanded.contains(ROOT_KEY) {
134 self.append_children(ROOT_KEY, 1, &mut rows);
135 }
136
137 self.tree_view
138 .update(&mut *cx, |tree, cx| tree.set_rows(rows, cx));
139 }
140
141 fn append_children(&self, parent: &str, depth: u32, out: &mut Vec<TreeRow>) {
142 let Some(children) = self.children.get(parent) else { return };
143 for entry in children {
144 let kind = match entry.display_type {
145 DisplayType::Folder => RowKind::Branch,
146 _ => RowKind::Leaf,
147 };
148 let icon = match entry.display_type {
149 DisplayType::Folder => "📂",
150 DisplayType::File => "📄",
151 DisplayType::Stream => "📡",
152 };
153 let is_expanded = self.expanded.contains(&entry.id);
154 out.push(TreeRow {
155 id: RowId::new(entry.id.clone()),
156 label: entry.name.clone(),
157 depth,
158 kind,
159 expanded: is_expanded,
160 icon: Some(icon.to_string()),
161 });
162 if is_expanded {
163 self.append_children(&entry.id, depth + 1, out);
164 }
165 }
166 }
167
168 fn on_tree_event(&mut self, event: &TreeEvent, cx: &mut Context<Self>) {
169 match event {
170 TreeEvent::ChevronToggled(id) => {
171 let key = id.as_str().to_string();
172 if !self.expanded.remove(&key) {
173 self.expanded.insert(key.clone());
174 self.load_children(key, cx);
175 }
176 self.push_rows(cx);
177 }
178 TreeEvent::RowClicked(id) => {
179 let key = id.as_str();
180 if key == ROOT_KEY {
181 return;
182 }
183 if let Some(entry) = self.find_entry(key) {
184 if !matches!(entry.display_type, DisplayType::Folder) {
185 cx.emit(DatabaseExplorerEvent::EntitySelected {
186 id: key.to_string(),
187 });
188 }
189 }
190 }
191 TreeEvent::RowDoubleClicked(id) => {
192 let key = id.as_str();
193 if key == ROOT_KEY {
194 return;
195 }
196 if let Some(entry) = self.find_entry(key) {
197 if !matches!(entry.display_type, DisplayType::Folder) {
198 cx.emit(DatabaseExplorerEvent::EntityOpened {
199 id: key.to_string(),
200 });
201 }
202 }
203 }
204 TreeEvent::ContextMenuRequested { .. } | TreeEvent::ActiveChanged(_) => {}
205 }
206 }
207
208 fn find_entry(&self, id: &str) -> Option<&EntityNode> {
209 for entries in self.children.values() {
210 if let Some(e) = entries.iter().find(|e| e.id == id) {
211 return Some(e);
212 }
213 }
214 None
215 }
216}
217
218impl Render for DatabaseExplorer {
219 fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
220 let theme = Theme::global(cx).clone();
221 let pending_count = self.pending.len();
222
223 div()
224 .size_full()
225 .bg(theme.bg_panel.clone())
226 .flex()
227 .flex_col()
228 .child(
229 div()
230 .h(px(28.0))
231 .px(px(8.0))
232 .border_b_1()
233 .border_color(theme.border)
234 .flex()
235 .flex_row()
236 .items_center()
237 .gap(px(6.0))
238 .child(
239 div()
240 .text_size(px(11.0))
241 .text_color(theme.fg_muted)
242 .child("🗄️"),
243 )
244 .child(
245 div()
246 .text_size(px(11.0))
247 .text_color(theme.fg_text)
248 .child(SharedString::from(self.db_path.clone())),
249 )
250 .child(
251 div()
252 .ml_auto()
253 .text_size(px(10.0))
254 .text_color(theme.fg_muted)
255 .child(SharedString::from(if pending_count > 0 {
256 format!("⏳ {}", pending_count)
257 } else {
258 String::new()
259 })),
260 ),
261 )
262 .child(if let Some(err) = self.open_error.clone() {
263 // Si la DB no abrió, mostramos el error y no pintamos el
264 // árbol vacío — sería confuso.
265 div()
266 .p(px(12.0))
267 .text_size(px(11.0))
268 .text_color(theme.accent_strong)
269 .child(SharedString::from(format!("error abriendo DB: {}", err)))
270 } else {
271 div().flex_grow().min_h(px(0.0)).child(self.tree_view.clone())
272 })
273 }
274}
275
276fn sort_entries(entries: &mut Vec<EntityNode>) {
277 entries.sort_by(|a, b| {
278 let a_dir = matches!(a.display_type, DisplayType::Folder);
279 let b_dir = matches!(b.display_type, DisplayType::Folder);
280 b_dir
281 .cmp(&a_dir)
282 .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
283 });
284}